Strategies
QuantRocket DemoMLStrategy port: pooled 25-horizon return features, numpy logistic 'beats the cross-sectional median over 22 open-to-open days' classifier, hold the top-3 probabilities, monthly equal-weight long-only rebalance at the next session open (USEQ 1-DAY, 9 Select Sector SPDRs)
Outcome: Abandoned
QuantRocketDemoMLSectorSpdrUseq
Outcome Summary
This strategy ported QuantRocket's DemoMLStrategy onto the nine Select Sector SPDRs. It used a numpy logistic 'beats the median' classifier to pick the top 3 ETFs each month. The backtest was profitable in absolute terms (+241.4%, Sharpe 0.37, average trade 1.01%, well above costs), but over the development period it grew 2.42x against 4.30x for an equal-weight basket of the same ETFs, with a larger 62.8% drawdown and a signal IC indistinguishable from noise. The analyst abandoned it at backtest review after one iteration, concluding that tuning could not create the missing selection skill, so optimization and later stages were never reached.
Hypothesis
Implementation
Stratmill port of QuantRocket's DemoMLStrategy onto the nine Select Sector SPDRs (USEQ daily). Every session it computes the 25 close-to-close return features per ETF. A pooled L2 logistic regression (numpy Newton/IRLS, sklearn-style C, features z-scored on the training set) is trained on whether each ETF's 22-day open-to-open forward return beats the cross-sectional median. Training is an expanding walk-forward: the first fit needs 756 sessions with fully known labels, then the model is refit every 12 calendar months. On the last trading session of each month the ETFs are ranked by predicted probability and the top 3 are held at equal weight (97% gross, 1x, long only). Orders decided at that close fill at the next session's open. The signal is the mean probability of the top-ranked names minus the median probability.
Verification Results
Submit the buys on the month-end bar as well, sized from the room left after the planned exits (equity*gross - value of kept holdings). If margin checks on pending orders make that unsafe at 1x, state the t+2 entry for new names in the description.
When the month-end rebalance needs any sell (a name leaves the book or a kept name is trimmed), calculate_signal returns before buying. The incoming names are bought on the next session's close and fill at the open of the session after that (t+2), not at t+1 as the source's weights.shift() does. This is a one-session lag on part of each monthly turnover. It is small next to a 22-day hold, but the description does not disclose it.
Set a sells_pending flag and run _rebalance_sells on the next session where no orders are pending.
If orders are still pending on a month-end session, only target_names is updated. Later sessions call _buys only (no _rebalance_sells), so names that dropped out of the book are not sold until the following month-end. Under simulated full market fills this is very unlikely.
Optionally retry the fit no more than once per session month after a failure.
Until the first successful fit, _labelled_rows (a list comprehension over deque indexes) and the fit attempt run on every bar, and again on every bar if _fit keeps returning False. Measured sandbox runtime is fine (11 s), so this is a performance note only.
Nine highly correlated sector ETFs give the classifier 9 cross-sectional observations a day, and 22-day labels overlap heavily, so there are few effective independent samples per annual refit. A 25-feature logistic model on that pool may barely beat noise. The book is 97% invested long at all times, so its drawdown will track SPY (2008 or 2020 could approach the 40% cap). In the backtest review, compare against an equal-weight 9-ETF benchmark, not against cash.
Backtest Review
- Sharpe
- 0.37
- Total return
- 241.36%
- Max drawdown
- 62.76%
- Trades
- 483
- Win rate
- 59.6%
- Profit factor
- 1.40
The code matches the port. It is long-only and uses the 9 sector SPDRs. Each month it buys the top 3 by logistic probability at about one third of equity each. The book is rebalanced on the last session of the month and filled at the next open. The model is refit once a year on labels that were fully known at the time. There are 358 closed trades, all long, which fits roughly 3 names changing over 18 years.
On its own terms the result clears the cost floor: average trade 1.04% of notional against the USEQ floor of 0.05%, profit factor 1.37, Sharpe 0.34, total return +141% from Dec 2002 to Mar 2021 (development period only).
The classifier does worse than holding all 9 ETFs equally. Over the same development window (detail file, both price-only, so dividends are excluded from both), the equal-weight basket grew 4.30x (92.4k to 397.6k). The strategy grew 2.42x (100k to 242.4k). The strategy beat the basket in only 6 of 19 calendar years, and it trailed in 2004, 2007, 2009 and 2016 by 9 to 14 points.
Its drawdown is worse than the market's, not better. The strategy's max drawdown is 62.8%, against 51.5% for the equal-weight basket (1.22x). It is also far above the 40% max_drawdown_pct registered in the research contract. The longest drawdown lasted 2,412 days, and Calmar is 0.08.
Picking 3 of 9 adds concentration risk without adding return. The positive headline Sharpe is lower-volatility market beta minus a selection penalty, not an edge.
Outcome Summary
A cross-sectional selection model needs to beat the equal-weight basket of its own universe, not just make a profit, because in a fully invested long-only sector book the absolute return can be market beta alone.
The analyst abandoned it at backtest review, before optimization, because the premise was falsified: the classifier's picks did worse than holding all nine ETFs equally, beat the basket in only 6 of 19 years and had a deeper drawdown (62.8% vs 51.5%, against a registered 40% limit). Fees were not the cause, and the code was judged a correct port.
A port of QuantRocket's DemoMLStrategy: a numpy L2 logistic classifier, trained on 25 return-horizon features and refit once a year, predicts which of the nine Select Sector SPDR ETFs will beat the cross-sectional median over the next 22 open-to-open days; each month it holds the top 3 at equal weight, long-only on USEQ daily bars.
The full backtest (Dec 2002 to Oct 2026, 483 long trades) returned +241.4% with a Sharpe of 0.37, an average trade of 1.01% of notional, profit factor 1.40, beta 0.93 and a 62.8% max drawdown. On the development period (Dec 2002 to Mar 2021) the strategy grew 2.42x, while an equal-weight basket of all nine ETFs grew 4.30x, and the signal-research IC was -0.05 (t=-0.8).
Analysis
DemoMLStrategy
Analysis
demo-ml.py
Installation and Deployment
Time series / date functionality#
Backtest and paper results are hypothetical. Trading involves risk of loss.