Skip to content
All strategies

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

Classification: (a) concrete, testable rule. The abstract makes two claims: (1) on a MONTHLY horizon, ranking assets by the maximum drawdown (MDD) of the formation window and buying the smallest-MDD names beats classic cumulative-return momentum; (2) on a WEEKLY horizon, recovery-based ranking detects mean reversion. We distil ONLY claim (1). Claim (2) is a weekly cross-sectional contrarian book; weekly reversal on crypto is in the dead mean_reversion / multi-leg-neutral family (L113, L137), so it is not……Show moreShow less

Classification: (a) concrete, testable rule. The abstract makes two claims: (1) on a MONTHLY horizon, ranking assets by the maximum drawdown (MDD) of the formation window and buying the smallest-MDD names beats classic cumulative-return momentum; (2) on a WEEKLY horizon, recovery-based ranking detects mean reversion. We distil ONLY claim (1). Claim (2) is a weekly cross-sectional contrarian book; weekly reversal on crypto is in the dead mean_reversion / multi-leg-neutral family (L113, L137), so it is not proposed. Claim (1) is re-expressed long-only on BINANCE_SPOT, the right venue for unleveraged long-only rotation (spot is 10.9% of the corpus against a >=15% target). Shorting the high-MDD leg is left out because multi-leg neutral books have 0 survivors (L137). Universe (point-in-time, per L151): the developer loads a candidate pool of Binance spot USDT pairs as extra_instruments. At each monthly rebalance, a name is ELIGIBLE only if it has >= 60 daily bars of history as of that date and ranks in the top 20 by trailing-30-day dollar volume (close*volume) as of that date. Names not yet listed, or with stale/missing bars, are excluded automatically. The pool is itself a selection choice made in 2026, and we state it openly: it is the 20 largest spot USDT pairs, and the point-in-time filter decides membership on each date. Formation: for each eligible name, compute MDD over the trailing 30 daily closes: MDD = max over t of (1 - close_t / running_max_close_up_to_t). Rank ascending by MDD (smallest drawdown = strongest). Holding: the 5 lowest-MDD names, equal weight at 19% of equity each (95% gross, 1x, cash account). There is ONE absolute condition and it provides the declared flat state: a selected name whose 30-day cumulative return is <= 0 is replaced by cash, not by the next name. A near-zero-MDD name can still have a slightly negative return in a grinding decline, so this condition takes the book to cash in bear months. Expected flat fraction is ~25-35% of calendar time, concentrated in 2022 and similar regimes. We note, per L130, that this is a condition: it is part of the pre-registered primary rule because an ungated long-only alt basket would carry ~70% drawdown in 2022, and that is not a deployable hypothesis. Variant B (ungated, pure MDD rank) is pre-registered as a separately scored comparison. The falsifiable content is the paper's own head-to-head: the MDD-ranked book must beat an identically constructed 30-day cumulative-return-ranked book on OOS Sharpe. That return-ranked book is essentially the existing spot rotation, which is at Sharpe -2.14 in paper. If MDD ranking does not beat it, the paper's claim does not transfer to crypto. Implementation note (7 recent sandbox_timeout deaths): keep a fixed 60-slot ring buffer of closes/volumes per instrument in on_bar/on_extra_bar, and compute MDD and dollar volume ONLY on the rebalance bar (O(20*30) once a month). Never rescan the full history per bar.

Both feedback items were checked against the platform itself. (1) The window stopping at 2024-06-17 is NOT a bar budget and cannot be fixed in strategy code. All 20 legs have daily catalog bars through 2026-09-28 (checked in data/catalog). The backtest agent passes end=final_exam_boundary (backtest_agent.py:341, pipeline_processor._final_exam_boundary_iso -> src/optimization/vault). The hypothesis's locked final-exam vault starts at about 2024-06-17. By design, [boundary, catalog end) is read exactly once, at……Show moreShow less

Both feedback items were checked against the platform itself. (1) The window stopping at 2024-06-17 is NOT a bar budget and cannot be fixed in strategy code. All 20 legs have daily catalog bars through 2026-09-28 (checked in data/catalog). The backtest agent passes end=final_exam_boundary (backtest_agent.py:341, pipeline_processor._final_exam_boundary_iso -> src/optimization/vault). The hypothesis's locked final-exam vault starts at about 2024-06-17. By design, [boundary, catalog end) is read exactly once, at sign-off (_take_final_exam), with the optimised params. Extending the research window would spend the holdout. If the analyst wants 2024-H2..2026 in the research sample, the operator has to move or unlock the vault. (2) rank_by in {mdd, return} is already implemented (see _rank). Please run the twin as the same config with parameters.rank_by='return'. The only code change this iteration is behaviour-neutral: _rank indexes the ring buffer directly instead of copying the whole deque, which trims rebalance cost after the last sandbox wall-time retry. Config, universe, parameters and trading logic are byte-for-byte the same as the previous run, so results are directly comparable.

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.