Skip to content
All strategies

Strategies

Short-Lag Industry-Factor Momentum: Time-Series Sign of Each Sector-minus-SPY Spread's Last-Month Return, SPY-Hedged Long-Short Book (9 SPDR sectors + SPY, USEQ 1-DAY, 21-session rebalance)

Outcome: Abandoned

IndustryFactorShortLagMomentumSpyHedged

Outcome Summary

IndustryFactorShortLagMomentumSpyHedged tested whether industry factors (sector ETF minus SPY) keep moving in the same direction at a one-month lag, using a beta-neutral, SPY-hedged book with fixed 0.9 gross per side and an 8% book-level stop. After 3 iterations, the full backtest still could not run because XLK.USEQ daily bars had 1490 missing expected bars, including the requested boundaries. The pipeline abandoned the run as a data failure, not a judgement of the strategy, so the backtest-review, analyst and risk stages were never reached. The hypothesis is still untested and can be revived once the XLK data gap is fixed.

Hypothesis

Source: Falck et al., 'Is Factor Momentum More than Stock Momentum?' (arXiv 2009.04824, 2020), abstract. The abstract's testable claim is that factor momentum exists both directionally (time-series) and cross-sectionally. It also claims that after controlling for stock momentum, only implementations that include the LAST MONTH of factor returns keep significant Sharpe ratios. Stock-level returns mean-revert at short lags, but factor returns continue at all lags. Our venues hold no anomaly-factor ETFs; the catalog……Show moreShow less

Source: Falck et al., 'Is Factor Momentum More than Stock Momentum?' (arXiv 2009.04824, 2020), abstract. The abstract's testable claim is that factor momentum exists both directionally (time-series) and cross-sectionally. It also claims that after controlling for stock momentum, only implementations that include the LAST MONTH of factor returns keep significant Sharpe ratios. Stock-level returns mean-revert at short lags, but factor returns continue at all lags. Our venues hold no anomaly-factor ETFs; the catalog was checked and MTUM/QUAL/USMV/VLUE/IWD/IWF/RSP are all absent. We therefore use the factor class we can build honestly from data on hand: industry factors, each defined as a sector ETF's return minus SPY's. The nine original Select Sector SPDRs (XLK, XLF, XLE, XLV, XLP, XLU, XLI, XLY, XLB) have USEQ 1-DAY bars from Dec-1998, about 27 years. XLRE and XLC are excluded because their history is too short. On every 21st session (counted in bars, not calendar dates), compute each spread's trailing 21-session return, f_i = ln(P_i,t/P_i,t-21) - ln(SPY_t/SPY_t-21). Time-series mode: hold sector i at weight sign(f_i)/9 x gross_fraction, and hold SPY at minus the sum of the sector weights. The SPY leg therefore nets the market out, and the book is a pure bet on industry-factor continuation. Hold for 21 sessions, then recompute. The abstract also tests a cross-sectional variant: long the top 3 and short the bottom 3 spreads by f_i, equal weight, with the SPY leg netting any residual. It is exposed as one categorical switch because both implementations come from the source; there is no other added degree of freedom. The universe is fixed at 10 names, within the ~10-name cap from lesson L173, so per-rebalance compute is trivial (10 log returns). How this differs from the pipeline's LargeSmallCapMonthlyMomentumSpread: that bet is a single size spread. This book is a nine-factor industry book, and the paper's specific claim is the 1-month lag, where stock-level reversal and factor-level continuation pull in opposite directions. What the full paper must confirm: (1) the 1-month formation window and monthly holding period, assumed here from 'last month of returns'; (2) that industry factors behave like their 72 anomaly factors (Moskowitz-Grinblatt 1999 and Arnott et al. 2021 report the same short-lag industry and factor continuation); (3) whether they skip any days between formation and holding (we skip none). Sizing: gross_fraction is FIXED at 0.9 of equity per side, with no leverage tuning and no buying-power parameter. A recent SPY strategy died because its Sharpe changed with position size; this design does not use the margin buffer at all. Net market beta is about 0 by construction. Stop rule: a catastrophic book-level stop flattens everything if the book is down 8% from the last rebalance's equity, and the book re-forms at the next scheduled rebalance.

This is a type (a)/(b) source item: the abstract states a testable rule (factor momentum, directional and cross-sectional, with the edge concentrated in the last month). USEQ holds only 4.9% of the corpus, against BINANCE's 63%. The book is long-short (that bucket is 15.7% vs a ≤55% long-only target) and market-neutral. It fixes 10 names per L173, gets decades of daily history, and has near-zero commissions. Factor ETFs are absent from the catalog, so we use industry factors (sector minus SPY), the closest honest proxy. Only two degrees of freedom are tuned, both justified by the source: the formation window around one month, and the time-series vs cross-sectional implementation. Gross exposure is fixed, so the optimizer cannot buy Sharpe with leverage.

negative_expectancy / premise_falsified on the first measured report. Code: no_edge (not fee_edge, because the gross edge is itself negative). The test ran 25 years (1999-2024) on 9 SPDR sector-minus-SPY spreads in time-series sign mode. Results: 1478 lots, avg_trade_return_pct -0.351% against a USEQ floor of 0.05% (about 0.10% for a two-sided hedged book), PF 0.959, Sharpe -0.05 with a CI of [-0.41, +0.29], total return -20.3%, max drawdown 34.5% against the pre-registered 20%. Adding back borrow (6.0%) and……Show moreShow less

negative_expectancy / premise_falsified on the first measured report. Code: no_edge (not fee_edge, because the gross edge is itself negative). The test ran 25 years (1999-2024) on 9 SPDR sector-minus-SPY spreads in time-series sign mode. Results: 1478 lots, avg_trade_return_pct -0.351% against a USEQ floor of 0.05% (about 0.10% for a two-sided hedged book), PF 0.959, Sharpe -0.05 with a CI of [-0.41, +0.29], total return -20.3%, max drawdown 34.5% against the pre-registered 20%. Adding back borrow (6.0%) and impact (7.3k) still leaves gross return at about -7%. The executed signal's IC is -0.011 (t = -0.19), so 1-month industry-factor continuation is absent on liquid SPDR sectors. QA's edge concern is borne out and is worse on the full history than on the sandbox's 1653 days (Sharpe 0.18). Benchmark: market-neutral, so buy-and-hold is not comparable. The only untested lever is the categorical cross_sectional mode, which ranks the same 9 spreads. With the time-series signal showing zero IC, it is not worth a 2-hour optimization. If research re-proposes factor momentum, it needs true anomaly-factor returns (for example Ken French portfolios), not sector ETFs.

Implementation

This book trades nine sector-minus-SPY industry factors (XLK, XLF, XLE, XLV, XLP, XLU, XLI, XLY, XLB vs SPY) on USEQ daily bars. Each factor is the sector's trailing log return over lookback_days aligned sessions minus SPY's over the same window. In time_series mode the book holds sector i at sign(f_i)*0.9/9 of mark-to-market equity, and SPY at minus the sum of the sector weights, so it is market-neutral. In cross_sectional mode it goes long the top 3 and short the bottom 3 spreads at 0.9/3 each, with SPY netting the residual. The book rebalances once per calendar bucket of rebalance_bars weekdays, (days_since_epoch*5//7)//21. This anchors the cadence to the bar timestamp so it survives restarts. It only approximates 21 trading sessions: holidays shift it by about 1 session per month, so formation and holding windows are not exactly equal. A book stop flattens every leg when mark-to-market equity (balance plus the unrealized PnL of all legs at their latest close) falls 8% below the equity at the last rebalance. The book re-forms at the next rebalance.

Iteration 3 fixes the QA findings. (1) Critical: the book stop and rebalance sizing used realized cash balance, which made the stop inert. They now use _mtm_equity(): account balance plus the sum of (last_close - avg_px_open) * signed_qty over every open leg. On a USEQ margin account the balance excludes unrealized PnL, so this is the true book value, and a mark-to-market 8% drawdown within the holding window now triggers the flatten. (2) Warning: when a leg keeps its sign and the size change exceeds $500,……Show moreShow less

Iteration 3 fixes the QA findings. (1) Critical: the book stop and rebalance sizing used realized cash balance, which made the stop inert. They now use _mtm_equity(): account balance plus the sum of (last_close - avg_px_open) * signed_qty over every open leg. On a USEQ margin account the balance excludes unrealized PnL, so this is the true book value, and a mark-to-market 8% drawdown within the holding window now triggers the flatten. (2) Warning: when a leg keeps its sign and the size change exceeds $500, _reconcile now submits only the delta (want - have) on the netted position. It closes and reopens only on a sign flip or when the leg goes to zero. This roughly halves turnover and spread/impact cost on the SPY hedge and on unchanged-sign sectors. (3) The calendar-bucket approximation is now documented in the docstring and description. Signal, universe, fixed parameters and tunable domains (mode, lookback_days 15-30) are unchanged. Caveat on the ledger: delta adjustments scale a netted lot, so one closed position can span several rebalances (lesson L175), and per-trade figures describe lots, not rebalances.

Verification Results

Acceptable as it stands. For an exact match, derive the bucket from a session index taken from the XNYS calendar (src/data/market_calendar.py) applied to bar.ts_init.

The hypothesis counts the 21-session rebalance in bars. The code instead anchors it to a calendar bucket, (days_since_epoch*5//7)//21. Check 20 requires a calendar anchor, and the developer documented the approximation. Holidays shift the bucket by about 1 session a month, so the 21-row formation window and the holding window are not exactly equal.

In the full backtest, check for OrderDenied events on the SPY hedge leg. If any appear, set config.leverage to 2.0, which is inert in sizing only if the parameter `leverage` stays at 1.0.

Gross notional reaches 1.8x equity: 0.9 long plus 0.9 short in both modes, or the SPY hedge at -0.9 when all sectors share a sign. config.leverage is 1.0. This matches the hypothesis ('0.9 of equity per side') and stays under Reg-T 2x. However, if the engine's margin model at default_leverage 1.0 denies the later legs, the book would be left partially unhedged. The sandbox's balanced 181 long / 181 short ledger suggests fills went through.

When the analyst compares per-trade return with the cost floor, use fills and turnover rather than closed lots.

Positions are netted, and same-sign rebalances trade only the delta, so one closed position can span several rebalances. total_trades (362 closed lots) is not the number of per-rebalance trades. 916 fills over about 54 rebalances x 10 legs is consistent with the design, so the ledger is not under-booked (L133 is not triggered), but per-trade economics describe lots.

The unoptimized sandbox ran 1653 days and returned +8.3%, with Sharpe 0.18, PF 1.086 and avg_trade_return_pct 0.107% over 362 netted lots. Impact cost was 11.2% of gross and borrow was 1.83% of equity, so most of the gross edge went to costs. A 1-month industry-continuation spread on liquid SPDRs is thin. First check whether net Sharpe survives once borrow and impact are applied across the full 27-year history, and check the normal-vol regime, where the sandbox lost 5.9%.

Backtest Review

Sharpe
-0.05
Total return
-20.31%
Max drawdown
34.47%
Trades
1478
Win rate
45.9%
Profit factor
0.96

The implementation follows the hypothesis. It holds 9 sector-minus-SPY spreads with an SPY hedge, rebalances on a monthly bucket, and has balanced long and short legs (739/739). Bars are aligned and equity is marked to market.

The sample is large and reliable: 1478 closed lots and 6,275 effective daily observations from 1999 to 2024, with accounting reconciled.

Negative expectancy on a large sample. Sharpe is -0.05 (95% CI -0.41 to +0.29), PF is 0.96, avg_trade_return_pct is -0.35% against a USEQ floor of 0.05% (about 0.10% for a two-sided hedged book), and total return is -20.3%. Under lesson L141, a negative Sharpe on more than 100 trades is a completed measurement.

The edge is negative even before costs. Adding back borrow (5.97%) and impact (about 7.3%) still leaves gross return at roughly -7%, so fees are not what killed it.

The executed forecast carries no information: IC is -0.011 (t = -0.19) over 299 non-overlapping 21-day labels, and the signal-research verdict is rejected_no_information.

Max drawdown is 34.5% (pre-registered constraint: 20%), and the book has been underwater for about 24 years. It lost money in the normal regime (-5.1%) and the stressed regime (-19.3%). Calendar years split 11 positive and 14 negative.

Benchmark not meaningful (market-neutral, beta about 0 by construction); judged on absolute metrics only.

Iteration History

Verification failed (Layer 1.5 — hypothesis/config consistency) [class=hypothesis_mismatch]: - optimization_plan fixes leverage at 1.0

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] The sandbox smoke test returned +10% over about 1650 sessions, with Sharpe 0.21 and a Sharpe CI of [-0.59, 0.95]. PF was 1.08 on 553 closed lots. Calm and stressed regimes were positive, but the normal regime was negative. impact_cost_pct is 11.5% of gross, and borrow cost is 1.8%. The edge is thin and sensitive to costs. Part of the turnover is self-inflicted by the close-whole-lot-and-reopen resize (see warning). - [critical] The……Show moreShow less

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] The sandbox smoke test returned +10% over about 1650 sessions, with Sharpe 0.21 and a Sharpe CI of [-0.59, 0.95]. PF was 1.08 on 553 closed lots. Calm and stressed regimes were positive, but the normal regime was negative. impact_cost_pct is 11.5% of gross, and borrow cost is 1.8%. The edge is thin and sensitive to costs. Part of the turnover is self-inflicted by the close-whole-lot-and-reopen resize (see warning). - [critical] The hypothesis's catastrophic book stop (flatten when the book is down 8% from the last rebalance's equity) is effectively inert. It compares self.get_account_equity() with self._rebal_equity. get_account_equity() calls resolve_account_equity(), which returns AccountBalance.total of the USEQ MARGIN account. In NautilusTrader that is the cash balance: it moves only on REALIZED PnL and excludes the unrealized PnL of open positions. This book opens all 10 legs at a rebalance and holds them until the next one, so realized equity barely moves within the holding window. The book could lose 15% mark-to-market and the stop would never fire. Losses are realized only at the next rebalance, and that rebalance resets _rebal_equity and _book_stopped anyway. The hypothesis names this stop explicitly, so a stop that cannot trigger counts as a missing stop. (line 197) — fix: Compute mark-to-market equity: balance plus the sum of unrealized PnL over open positions. Use self.portfolio.unrealized_pnls(venue), or sum position.unrealized_pnl(Price) at each leg's latest aligned close. Use that value both for the 8% stop and for _rebal_equity at the rebalance. Sizing at the rebalance should also use MTM equity; current sizing on realized balance is only correct when the book is flat. - [warning] In _reconcile, when a held leg keeps its sign but its target size changes by more than min_adjust_notional ($500), the code closes the entire lot and reopens the new size. It does not trade the delta. On 0.9/9 = 10% legs this happens at almost every rebalance, so the SPY hedge and the unchanged-sign sectors are round-tripped in full. That roughly doubles turnover and spread/impact cost, and inflates the closed-lot trade count. (line 160) — fix: Submit only the delta (want - have) as a single order on the netted position. Close and reopen only on a sign flip. - [warning] The rebalance bucket is (days_since_epoch*5//7)//21, an approximation of weekdays that is not 'every 21st session counted in bars' as the hypothesis states. This calendar anchoring is acceptable, and is preferred under check 20 for restart and parity stability. Holidays shift the effective cadence by about 1 session per month. Formation (lookback_days rows) and holding (bucket) therefore do not align exactly. That does not change the mechanism. (line 194) — fix: No change is required. Document the approximation in the description.

Outcome Summary

Before a multi-leg USEQ strategy reaches backtesting, check that every leg (here XLK.USEQ 1-DAY) actually has full catalog coverage across the requested window, so a data gap does not use up iterations.

It was abandoned with reason backtest_failed:data after the full backtest failed 3 times with data_unavailable: XLK.USEQ-1-DAY-LAST-EXTERNAL was missing 1490 expected bars, including the requested boundaries. The pipeline marked this a data failure, not a verdict on the strategy, and said the strategy can be revived once the cause is fixed.

A SPY-hedged long-short book on the nine original Select Sector SPDRs that bets on short-lag industry-factor momentum: it takes the sign (time-series mode) or the rank (cross-sectional top-3/bottom-3 mode) of each sector-minus-SPY spread's trailing 21-session return and rebalances every 21 sessions, based on Falck et al. (2020).

There are no performance numbers. The full backtest never ran, so return, Sharpe and trade count do not exist and the optimization report is empty.

Analysis

Is Factor Momentum More than Stock Momentum?

Backtest and paper results are hypothetical. Trading involves risk of loss.