Strategies
Maximum-Drawdown-Ranked Monthly Momentum on Binance Spot: Hold the 5 Lowest-30-Day-MDD Names of a Point-in-Time Top-20 Liquidity Universe, Long-Only, Cash When Formation Return <= 0 (Choi 2014, DAILY bars)
Outcome: Abandoned
MddRankedMonthlyMomentumSpotTop5
Outcome Summary
The strategy tested long-only Binance Spot rotation based on selecting the lowest-drawdown names from a point-in-time liquid universe, with a cash condition for non-positive formation returns. No performance metrics were produced because Layer 3 verification timed out. After five iterations, the run was abandoned during verification and did not reach later pipeline stages.
Hypothesis
verification_loop: 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).
Implementation
Choi (2014) MDD momentum on Binance spot, daily bars. On the first daily bar of each calendar month, the pool of 20 spot USDT pairs is filtered point-in-time: a name needs at least 60 bars and a place in the top 20 by 30-day dollar volume. Survivors are ranked by 30-day max drawdown, ascending. The 5 lowest are held at 19% of equity each (long-only, 1x); any selected name whose 30-day return is <= 0 is replaced by cash. rank_by='return' is the pre-registered falsification twin: it ranks by 30-day cumulative return, descending, and leaves universe, cash gate, weights and schedule identical.
Verification Results
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).
Add a parameter rank_by in {'mdd','return'}. With 'return', sort by -ret instead of (mdd, -ret) in _rank(), so the comparison runs on the same code path.
The hypothesis pre-registers a head-to-head against an identically built 30-day cumulative-return-ranked book as its falsification test. Only Variant B (require_positive_return=false) can be selected by parameter. The return-ranked comparison book cannot be selected by parameter.
Optionally require the bar's day-of-month to be 1, or the first processed bar of a new month, before the first rebalance.
The first rebalance fires on the first bar where any name clears min_history_bars (60 bars). That can be mid-month rather than on the first bar of the month. After that, the month-first grid holds. This happens once per backtest or warm start and is harmless.
Acceptable as-is. If earlier coverage matters, add an explicit staleness check (last ingested ts == bar ts) and move later-listed names to a slower or unsynced leg mechanism.
The hypothesis excludes names with stale or missing bars. The code only checks the history count. In practice the base template's same-timeframe alignment barrier stops the primary signal until every leg has a bar at the current timestamp, so a stale leg cannot be ranked on old prices. The flip side is that the whole book waits for the latest-listed pool member (the 2020 covid window shows 0.0% return) and skips any date where one leg has a gap.
Analysis
(1) Rerun over the full available window through 2026-09. The report stops at 2024-06-17 although all 20 legs have daily bars to 2026-09-26. Check the backtest end date and bar budget; this may be a harness setting, in which case the operator needs to lift it. (2) Add parameter rank_by in {mdd, return}. With 'return', rank descending by 30d cumulative return, keeping the universe, cash gate, weights and schedule identical. Report both runs; the hypothesis stands only if MDD Sharpe > return-ranked Sharpe on the same window. No other logic changes. Metrics expected to move: effective span 3.7y -> ~6y and 2021's share of profit down. If the full-window MDD Sharpe falls below ~0.5 or it loses to the twin, expect abandon (premise_falsified). Details: workspace/discussions/1a5cf146-86a2-4fe3-b974-c3647e0fe164/iteration_1_feedback.md
Outcome Summary
Keep per-bar computation bounded and perform the monthly ranking only at rebalance time.
It was abandoned during verification after the sandbox backtest timed out. The recorded diagnosis was excessive per-bar work scaling with history, and later stages were not reached.
The strategy tried to rank the point-in-time top-20 Binance Spot USDT universe by lowest trailing 30-day maximum drawdown, hold the five best names long-only, and move selected names to cash when formation return was non-positive.
No backtest or optimization metrics were available because verification did not complete. After five iterations, its Layer 3 sandbox backtest exceeded the 300-second limit on approximately 20,000 bars.
Analysis
Maximum drawdown, recovery, and momentum
Backtest Review
- Sharpe
- 0.90
- Total return
- 296.20%
- Max drawdown
- 36.10%
- Trades
- 105
- Win rate
- 45.7%
- Profit factor
- 2.26
Backtest and paper results are hypothetical. Trading involves risk of loss.