Apex++GAF-DC2: specification
A momentum rotation into the 4 strongest S&P 500 / Nasdaq-100 stocks whose revenue growth is accelerating, with cash in the top half of recent filers and not among the most financially distressed, sized to a 60% volatility target, plus a 0.5x equity TQQQ sleeve. The sleeve is held only while QQQ's 20-day volatility is low. Account: US margin account at Interactive Brokers.
Data
| Item | Source |
|---|---|
| Stock prices | Daily open, high, low, close, adjusted for splits and dividends (total return) |
| Universe | Point-in-time S&P 500 and Nasdaq-100 membership (constituents as of each date, including later-delisted names) |
| Market cap | Point-in-time company market cap (all share classes) |
| QQQ, TQQQ | Daily total-return open, high, low, close |
| Fundamentals | Sharadar SF1, quarterly as-reported (ARQ): revenue, assets, cash (cashneq), debt, net income, current ratio, calendar quarter, filing date (column date) |
| Fed funds | Effective federal funds rate (FRED DFF) |
| Period | 2010-02-11 (first TQQQ session) to 2026-09-18 |
Account and costs
- Starting equity $100,000. On 2010-02-11 (session 0) the TQQQ sleeve and stock sleeve 0 are filled at the open, decided on the 2010-02-10 close. Sleeves 1-3 hold cash until their first rebalance (2010-02-19, 2010-02-26, 2010-03-05).
- Trading cost: 5 bps per side on traded notional. No taxes.
- Margin loan rate: fed funds + 1.50% on the first $100,000 borrowed, + 1.00% up to $1,000,000, + 0.75% above. Blended, 360-day year.
- Cash credit: fed funds - 0.50% on cash above $10,000; when equity is below $100,000, scaled by equity / $100,000.
Timing
| Item | Decided on | Trades at |
|---|---|---|
| Stock sleeve picks, weights, vol scale | close of day t-1 | open of day t |
| Stock sleeve rebalance | every 20 sessions per sleeve | open |
| TQQQ vol gate | every session, close of day t-1 | open of day t, the session after it flips |
| TQQQ resize to 0.5 x equity while on | 11th session of each month | open |
Rules
STOCK BOOK: 4 sleeves, each 1/4 of equity
Sleeve k (k = 0..3) rebalances on sessions 5k, 5k+20, 5k+40, ...
counted from 2010-02-11 (session 0).
On a sleeve's rebalance, using data through the prior close:
universe = point-in-time S&P 500 + Nasdaq-100 members; one share
class per company (the higher-ranked class)
eligible = close > SMA200 and SMA200 > its value 20 sessions earlier
and growth_accel > 0 (see Growth-acceleration filter)
and cash_ok and distress_ok (see Cash and distress filters)
rank = EMA50 / EMA200 of the close, highest first, eligible names only
picks = keep each name the sleeve already holds while it is eligible
and ranked 12 or better; fill the remaining slots, up to 4
names, with the highest-ranked eligible names not yet held
w_i = cap_i^2 / sum of cap^2 over the picks
(cap = point-in-time company market cap)
basket_vol = sample stdev (ddof 1) of the last 20 daily returns of the
portfolio sum(w_i * r_i), annualized x sqrt(252)
(r = simple daily total returns of the picks)
scale = clip(0.60 / basket_vol, 0.0, 1.5)
target_i = (equity at today's open / 4) * w_i * scale
Between rebalances, sleeve positions are not traded (they drift).
TQQQ SLEEVE
qqq_vol = sample stdev (ddof 1) of the last 20 daily QQQ total returns
x sqrt(252), on the prior close
gate starts ON
if ON and qqq_vol > 0.32: OFF -> sell all TQQQ at the open
if OFF and qqq_vol < 0.28: ON -> buy TQQQ to 0.50 x equity at the open
(between 0.28 and 0.32 the gate keeps its state)
While ON, also trade TQQQ to 0.50 x equity on the 11th session of
each month.
CASH
cash = equity - stocks - TQQQ. Negative cash is a margin loan.
At each rebalance at most 2.0x equity (1.5 stocks + 0.5 TQQQ); positions
drift between rebalances.
DELISTING
A held stock with no price on a session is converted to cash at its
last close.
Growth-acceleration filter
Data: Sharadar SF1 (fundamentals table, the full bulk download fundamentals.csv.zip, or the API), dimension ARQ (as-reported quarterly), fields revenue, calendardate and the filing date. The current Sharadar API and bulk files name the filing-date column date; older documentation calls it datekey. Example: ticker A, calendardate 1999-09-30, date 2000-01-25, reportperiod 1999-10-31.
For each company, one report per calendardate quarter (if duplicated, keep the earliest datekey).
growth(q) = revenue(q) / revenue(q-4) - 1 (requires revenue(q-4) > 0)
growth_accel(q) = growth(q) - growth(q-1)
q-k = the report whose calendardate is exactly k quarters before q, and
whose datekey is on or before q's datekey. Otherwise growth_accel is missing.
Point in time:
A report's value is usable from the first trading session strictly after its datekey.
It is carried forward until the company's next report becomes usable,
for at most 315 sessions; after that it is missing.
A secondary share class with no SF1 rows uses its company key's rows (GOOG -> GOOGL).
Eligibility: growth_accel > 0.
A missing value makes the stock ineligible.
The rank stays EMA50 / EMA200. The filter only removes names.
Cash and distress filters
Report universe: every ARQ report of every ticker that is a column of the backtest dataset (all securities that were ever S&P 500 or Nasdaq-100 members, 1997 to date), and of each column's company key (the primary share class, e.g. GOOGL for GOOG). In our dataset that is 1,328 tickers, 1,302 of which have SF1 rows. This set of reports is also the comparison pool for the percentiles below.
For each company, one report per calendardate quarter (if duplicated, keep the earliest date).
Per report (dk = its filing date):
A = assets (missing unless assets > 0)
cash = cashneq / A
distress = debt / A - netinc / A - currentratio
Any missing input -> that value is missing.
Flag, fixed once at filing:
pool_x(dk) = the reports of the report universe (all companies, this one included)
with a non-missing x and a filing date in [dk - 90 calendar days, dk]
cash_ok = cash > 50th percentile of pool_cash(dk)
distress_ok = distress < 90th percentile of pool_distress(dk)
Percentile: linear interpolation between closest ranks (numpy.percentile default).
A report with a missing cash (distress) value has no cash (distress) flag; it is
skipped, and the company's previous report with a value keeps applying.
Point in time (as GAF, per flag):
A report's flag is usable from the first trading session strictly after dk.
It is carried forward until the company's next report with that value becomes
usable, for at most 315 sessions; after that it is missing.
A secondary share class with no SF1 rows uses its company key's rows.
Eligibility (added to Apex++GAF's): cash_ok AND distress_ok.
A missing flag makes the stock ineligible.
The rank stays EMA50 / EMA200. The filters only remove names: a filtered stock is neither bought nor kept by the buffer.
Margin model (IBKR Reg T with concentration stress)
maintenance = max( 0.25 x stock value + 0.75 x TQQQ value,
div(n) x sum_i stress_i x stock_value_i )
n = number of distinct companies held
div(n) = 1.00 for n <= 2, 0.95 for 3-5, 0.75 for 6-9, 0.60 for 10+
stress_i = min(1, max(0.30,
0.30 + 0.065 x max(0, ret1y_i - 0.50),
0.50 x vol30_i))
ret1y_i = 252-session total return
vol30_i = population stdev (ddof 0) of the last 30 daily log returns x sqrt(252)
initial margin on any purchase:
equity >= max(0.50 x long market value, maintenance rule sum, 1.1 x stress term)
after the trade; purchases are scaled down pro rata to satisfy it.
maintenance check: at every close, and at the daily lows (all positions
marked at their low). A deficit is a breach: at the next open, sell all
positions pro rata until equity >= 1.10 x maintenance.
Results
Sharadar data, IBKR margin model as above, 2026-10-09.
In-sample backtest, 2010-02-11 to 2026-09-18. The two filters and the two DC2 parameters were chosen after seeing this period, so these numbers are optimistic.
| CAGR | MaxDD | Rolling 4y mean / median | Worst 4y | Margin breaches | |
|---|---|---|---|---|---|
| Apex++GAF-DC2 | 90.3% | -56.7% (2018-06-14 to 2018-12-24) | 81.8% / 82.4% | +36.9% | 0 |
| Apex++GAF-DC | 82.3% | -55.9% | 75.5% / 75.5% | +36.0% | 0 |
| Apex++GAF | 71.4% | -62.1% | 66.8% / 65.0% | +31.6% | 1 |
Walk-forward, post-2010 regime only. Each configuration choice uses training windows from 2010-03 onward and is scored on the next untouched period (2015-18, 2019-22, 2023-26). The distress and cash filters were chosen in every fold; the DC2 parameters were chosen in every fold of a 12-configuration grid.
| 4-year windows starting 2015-01 to 2022-09 | Rolling 4y mean / median |
|---|---|
| Apex++GAF-DC2 | 78.1% / 76.8% |
| Apex++GAF-DC | 70.1% / 67.8% |
| Apex++GAF | 66.8% / 63.5% |
Calendar-year returns, Apex++GAF-DC2 (in-sample):
| 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 |
|---|---|---|---|---|---|---|---|---|
| +111% | -13% | +116% | +238% | +84% | +80% | +14% | +177% | -25% |
| 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 (to 09-18) |
|---|---|---|---|---|---|---|---|
| +86% | +201% | +108% | -16% | +97% | +366% | +60% | +185% |
Robustness. Prices of the largest contributors match Yahoo Finance on every held day. Doubling trading costs to 10 bps lowers the 2015-2026 rolling 4-year mean by about 1.3 points; filling at the close instead of the open, or one session later, changes it by under 4 points. The book is concentrated: about 6 names on average, and NVDA is the largest single contributor. Without NVDA in the universe, the 2015-2026 rolling 4-year mean is about 65%.
Broker sensitivity (in-sample CAGR). IBKR's portfolio-level margin gives the most room for this mix of stocks and TQQQ: 90.3%. Standard per-position rules with TQQQ at 75% maintenance: about 85-90%, with up to 10 forced partial sales. With TQQQ not marginable: about 77%.