Skip to content

View original

SpotLargeCapDualMomentumRotationLong

Hypotheses

Spot Large-Cap Dual-Momentum Rotation, Long-Only (Multi-Instrument BINANCE_SPOT Basket, Daily Bars / Weekly Rebalance, Cross-Sectional Relative Strength + Absolute-Trend Cash Filter, No Leverage, Low-Turnover, Low-Parameter)

Hypotheses

A LONG-ONLY, MULTI-INSTRUMENT dual-momentum rotation across a small basket of liquid large-cap spot majors on BINANCE_SPOT (BTCUSDT, ETHUSDT, BNBUSDT, SOLUSDT), using daily bars and a WEEKLY rebalance. It combines the factory's single repeatedly-validated edge family (momentum/trend) with two robustness features the recent failures were missing: (1) it runs on the highest-survival, most under-weight venue — BINANCE_SPOT (9.4% vs ≥15% target) — where majors have DEEP multi-year clean history (BTC/ETH since 2017, SOL/BNB since 2020), so the walk-forward and the held-out recent tail span genuine bull/bear/chop regimes rather than a single soft 6-month window (the exact flaw that killed the HL impulse and squeeze strategies); and (2) an ABSOLUTE-trend cash filter that rotates the book to USDT (out of the market) when no asset is in an uptrend, so it structurally avoids the all-negative-months death that broke the pure-OHLCV HL strategies. It is deliberately NOT any landmine family: NOT Deribit options (broken harness), NOT Hyperliquid OI/liquidation/tick microstructure (data wall), NOT a single-symbol intraday breakout/fade (transfers poorly to crypto's 24h tape), NOT funding-carry on decayed majors. Cross-sectional + absolute (‘dual’) momentum is one of the most widely-documented, regime-robust anomalies across asset classes, and crypto exhibits strong leadership rotation between majors (BTC-season vs alt-season) that this captures. Low turnover (weekly, with a hysteresis buffer) keeps spot fees (~0.20% RT) negligible, and the parameter count is tiny (4) to resist the overfitting that has recurred. Fills BINANCE_SPOT (under-weight venue) and multi-instrument scope (14.4%).

Hypotheses

Iteration 4 fixes the reported Layer-3 sandbox_timeout with the two smallest changes that matter, leaving the signal logic (which passed Layers 1-2) untouched. (1) Per-bar work is now O(1): each asset's closes live in a bounded deque(maxlen=lookback+1) appended once per bar (primary in calculate_signal, legs in on_extra_bar), so momentum is two deque reads instead of rebuilding a 500-element close list with .as_double() on every bar for every asset. min()/sorted() now touch only the 4-element asset list. (2) A 3% cash buffer plus a spend budget derived from actual free USDT + estimated sell proceeds. The previous version targeted 100% of total equity, overdrew the CASH account by exactly the commission, and the engine HALTED the run on AccountBalanceNegative in Aug-2020 — the sandbox was burning its whole wall-clock to produce a truncated 1.5-year run with 3 trades. (3) The weekly cadence is now anchored to the calendar (epoch-day modulo rebalance_days from bar.ts_init) rather than a process-relative bar counter, per the schedule rule — identical across restarts, backtests and paper. Profiling note for the reviewer: I ran the exact sandbox configuration locally under cProfile. Total 109s wall for the full 2400-day window; the strategy's own on_bar path accounts for 0.015s of it. 152s of the 178s profiled run is ParquetDataCatalog.bars() globbing data/catalog/data/bar (1.08M files / 101k dirs) — a fixed ~76s per catalog.bars() CALL, and every multi-instrument strategy pays exactly two (primary + batched extras). So the residual timeout risk is catalog-scan/page-cache-bound infrastructure, not strategy code; no further change inside the strategy can reduce it. Local full-window result after the fixes: 57 trades, total_return +1627%, Sharpe 0.92, avg_trade_return_pct 13.5%, max_dd 59% (unoptimized, whole 2019-2026 span, no early halt).

Hypotheses

Premise falsified on a full 9-year, adequate sample. The hypothesis claims the absolute-trend cash filter structurally avoids deep drawdowns and that dual-momentum rotation adds regime-robust alpha; the backtest shows neither. max_drawdown is 64.98% (>60%, well above the 50% bar) with an 889-day underwater stretch and 2022 annual -51.5% — the weekly-only exit lets intra-holding drawdowns run unchecked. Against the fair benchmark (equal_weight_basket, benchmark_meaningful=true) the strategy posts information_ratio -0.53 and only modest positive alpha carried by beta: it risk-adjusted UNDERPERFORMS simply equal-weighting the four majors, so the rotation edge is measurably absent. The +2469% headline is crypto beta (beta 0.23, 53.7% still unrealized MTM) and the avg_trade_return_pct of 19.1% on a no-leverage spot book is a compounding artifact from position size riding the equity curve, not per-name edge. This is the long-only spot-basket signature that is rejected on drawdown; optimizing the 4 parameters cannot lift a >60% DD under the promotion floor or turn negative IR into positive alpha. abandon_class=premise_falsified.

Implementation

Long-only dual-momentum rotation over four large-cap BINANCE_SPOT majors (BTC/ETH/BNB/SOL) on daily bars with a calendar-anchored weekly rebalance. Each rebalance ranks the basket by 90-day return (cross-sectional relative strength) and holds the top 2 names that are ALSO in their own absolute uptrend (momentum > 0); anything failing the absolute filter rotates to USDT cash, so a broad downtrend leaves the book flat instead of holding the least-bad loser. Equal weight per held name, a hysteresis band (a held name is kept while still ranked inside top_k+1 and still trending up), and a material-delta threshold keep turnover and the ~0.20% round-trip spot fee negligible. No leverage; SELL is only ever used to reduce/exit an existing long.

Verification Results

Risk is low/mitigated: _momentum() returns None until the deque reaches lookback+1, and None-momentum names are excluded from `eligible` in _rebalance, so no premature trades can occur regardless of whether the override is honored. Optionally confirm FactoryStrategy actually calls this override.

Verification Results

min_bars_required(self) shadows the base framework method (static-analysis base_shadow warning). If the base does not honor the override for warm-up gating, entries could in principle be evaluated before the 90-day lookback buffer fills.

Verification Results

No code change required — this matches the hypothesis. Noted so the analyst does not mistake the weekly-granularity drawdown for a filter bug.

Verification Results

The absolute-trend cash filter only rotates to USDT at the weekly (calendar-anchored) rebalance boundary, so a broad downdraft mid-week is held to the next rebalance. This is consistent with the hypothesis's stated low-turnover weekly design, but it means the 'sits out of the market in a downtrend' protection is granular to ~7 days, contributing to the ~60% sandbox drawdown.

Verification Results

Sandbox is strongly positive (total_return +1627%, Sharpe 0.99, avg_trade_return_pct 13.5%, PF 1.86) and the mechanism (dual momentum) is the factory's one repeatedly-validated edge family, so no profitability doubt at the mechanism level. Two things for the analyst to weigh on full history: (1) max_drawdown ~60% with a 917-day underwater duration and end_unrealized_pct -68.6% — the absolute-trend cash filter does NOT prevent deep intra-holding drawdowns because exits only fire at the weekly rebalance, not intra-week; (2) reported exposure_pct 138.78% looks inconsistent with a no-leverage 97%-deployable spot book — likely a turnover/MTM metric artifact rather than real over-investment (budget is hard-capped to free USDT + est. proceeds, so the code cannot overspend the CASH account), but worth a sanity check against the full-history equity curve. Advisory only.

Backtest Review

Deep multi-year clean spot history (BTC/ETH since 2017), so the sample spans genuine bull/bear/chop regimes

Backtest Review

Positive absolute return with PF 1.98 and 50 trades — the mechanism trades and is not degenerate

Backtest Review

Low, negligible fee drag (commission 1.51% of gross) as designed

Backtest Review

max_drawdown 64.98% (>60%, far above the 50% bar) with an 889-day underwater duration — the absolute-trend cash filter does NOT prevent deep drawdowns because exits only fire at the weekly rebalance (2022 annual -51.5%)

Backtest Review

information_ratio -0.53 vs equal_weight_basket with benchmark_meaningful=true — the rotation risk-adjusted UNDERPERFORMS simply holding the four majors equal-weight; the claimed cross-sectional/absolute rotation edge is absent

Backtest Review

avg_trade_return_pct 19.1% / avg_position_pct 56% / exposure_pct 143% on a no-leverage spot book is a compounding/leverage artifact (position size riding the equity curve), not per-name edge (L36)

Backtest Review

The +2469% headline is crypto beta (beta 0.23 riding the full cycle) and 53.7% of it is un-exited MTM (end_unrealized_pct)

Backtest Review

Only 50 trades over 9 years combined with >60% DD is the long-only spot-basket signature that has consistently been rejected on drawdown (L19/L49)

Analysis

Backtest gate: - metrics unreliable: 1 trades / 1092 data days is too little evidence (need >=5 trades over >=10 days) — increase signal frequency or widen the window

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['top_k', 'assets', 'lookback', 'resize_frac', 'min_notional', 'qty_precision', 'rebalance_days', 'hysteresis_band', 'default_qty_precision'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000) - steady_downtrend: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000) - flat_ranging: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000) - volatility_spike: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000) - zero_volume: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000) - price_gap: TypeError: SpotLargeCapDualMomentumRotationLong._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735695060000)

Iteration History

sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (2400 days of BTCUSDT.BINANCE_SPOT-1-DAY-LAST-EXTERNAL). This almost always means per-bar work that scales with history — e.g. rescanning the full funding/supplementary series, or rebuilding a list and calling min()/sorted() inside calculate_signal()/on_bar() on every bar. Precompute sorted timestamp arrays ONCE in __init__ and use bisect, or cache lookups keyed by timestamp, so per-bar cost is O(log n) not O(n).

Iteration History

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Advisory: the sandbox produced only 1 trade over 184 days because 90d warmup + top-2/hysteresis stickiness make turnover very low; not a blocker, but the analyst should confirm full history generates enough rotations to be measurable. - The weekly rebalance cadence is driven by a process/window-relative bar counter, not the bar's calendar timestamp: `self._rebal_count += 1; if (self._rebal_count - 1) % self._rebalance_days == 0: self._rebalance()`. calculate_signal only runs after min_bars_required (92 bars) and once per aligned primary timestamp, so _rebal_count starts at 1 on the FIRST post-warmup bar and rebalances fire on the 1st/8th/15th… processed bars counted from the window start. This is check-18's critical anti-pattern: (1) each walk-forward window and the holdout start on a different date, so the weekly grid lands on different calendar days per window — parity replay is broken and optimized results won't match full history; (2) a live crash-restart resets the counter and fires a full basket rebalance on the first bar, churning the book and re-anchoring to uptime. The class defines _DAY_NS but never uses it.
Strategy report

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