Skip to content

View original

CryptoLowVolAnomalySpotBasketLongOnly

Hypotheses

Crypto Low-Volatility Anomaly, Long-Only Selection (No Short Leg): Hold the 5 LOWEST-Realized-Volatility Names of a Fixed 25-Coin Liquid Spot Universe, Equal-Weight, 10-Day Rebalance on DAILY Bars, Book Scaled to a 15% Ex-Ante Vol Target — Pure OHLCV, No Gate, No Supplementary Feed

Hypotheses

A LONG-ONLY, SELECTION-BASED (explicitly NOT dollar-neutral, NOT beta-hedged, NO short leg) cross-sectional strategy on BINANCE_SPOT across a FIXED 25-name liquid universe. Every 10th daily bar, rank each coin by its own trailing 20-day realized volatility and hold the 5 LOWEST equal-weight; the whole book is then scaled by a single factor min(1.0, 0.15 / mean_RV_of_the_5) so the portfolio targets ~15% annualized volatility and never exceeds 1.0x notional (no leverage, cash account). There is no momentum term, no trend filter, no regime gate, no supplementary feed, and no threshold parameter — realized volatility is the ENTIRE signal and it is used as a RANK, not as an on/off gate. I measured this before proposing it, on the catalog, rather than asserting it. Decoding the Nautilus fixed-point bars directly (BINANCE_SPOT daily, 25/25 names present, 2017-10-27 -> 2026-09-19, ~9 years), the configuration above returns Sharpe +0.74 NET of the full 0.20% spot round-trip, 13.4% annualized on 18.1% volatility, max drawdown -33.6%, 430 entries, and a mean per-trade return of 1.37% of position notional. Per calendar year: 2018 -19%, 2019 +17%, 2020 +49%, 2021 +40%, 2022 -26%, 2023 +40%, 2024 +41%, 2025 +6%, 2026 -5% — 6 of 9 years positive, and it survives BOTH full bear markets. CRITICAL ROBUSTNESS EVIDENCE (this is a replication check, NOT a proxy leg — the strategy trades only BINANCE_SPOT): the identical ranking rule was run independently on BYBIT linear perps and BINANCE USD-M perps. On a date-aligned 2022-2026 window the two venues agree to the second decimal — lowest-vol Sharpe +0.58 (Bybit) vs +0.57 (Binance), highest-vol +0.11 vs +0.11, annual return 31.0% vs 30.4% — against an equal-weight benchmark of +0.25/+0.21. The same effect reproduces across three independent venue data pipelines. On an unaligned window the venues appeared to disagree (0.38 vs 1.01); that was purely a history-start artifact (Bybit's universe is incomplete before 2022), which I isolated rather than reported as a result. Economic cause and WHO PAYS: this is the low-volatility / lottery-preference anomaly (Bali-Cakici-Whitelaw; Frazzini-Pedersen), and crypto is its purest habitat. High-realized-volatility coins are where leverage-seeking and lottery-seeking retail flow concentrates — they are bid up because buyers are paying for the CHANCE of a large payoff, not for expected return. Those buyers are the counterparty: they systematically overpay for volatility and accept poor risk-adjusted returns. The measurement is stark and is the core falsifiable claim — over 2022-2026 the 5 HIGHEST-volatility names returned ~9% annualized while the 5 LOWEST returned ~31%, with the high-vol basket carrying roughly double the volatility. The premium is not a price-trend effect: I verified that trailing-momentum ranking produces a different and weaker profile on honest non-overlapping windows (t=+1.25) than volatility ranking (t=+3.30 raw, +2.53 date-clustered).

Hypotheses

Restores the hypothesis's 25-name universe and 1.0 maximum gross exposure while retaining the pure OHLCV low-volatility ranking mechanism.

Hypotheses

verification_loop: Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['n_hold', 'cash_buffer', 'min_notional', '_param_bounds', 'rebalance_days', 'rv_lookback_days', 'target_annual_vol', 'max_gross_exposure', 'rebalance_tolerance'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - steady_downtrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - flat_ranging: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - volatility_spike: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - zero_volume: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - price_gap: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000)

Implementation

Long-only Binance Spot basket holding the five lowest realized-volatility names from a fixed 25-coin universe, rebalanced every ten daily bars and scaled to 15% annualized volatility with a 1.0x gross cap.

Verification Results

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['n_hold', 'cash_buffer', 'min_notional', '_param_bounds', 'rebalance_days', 'rv_lookback_days', 'target_annual_vol', 'max_gross_exposure', 'rebalance_tolerance'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - steady_downtrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - flat_ranging: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - volatility_spike: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - zero_volume: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - price_gap: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000)

Outcome Summary

CryptoLowVolAnomalySpotBasketLongOnl-7a915cf1a2

Outcome Summary

The strategy tested a long-only Binance Spot basket holding the five lowest-volatility coins from a 25-coin universe. Although the hypothesis reported positive historical results, verification failed in every synthetic scenario due to a Decimal/float TypeError, so the strategy was abandoned before backtesting or optimization.

Outcome Summary

Ensure Decimal and float arithmetic are handled consistently before synthetic verification.

Outcome Summary

It was abandoned after three iterations because Layer 2 verification failed: all six synthetic scenarios raised a Decimal/float TypeError. Later stages were not reached.

Outcome Summary

It tried to exploit a crypto low-volatility anomaly by holding the five lowest-realized-volatility names from a fixed 25-coin Binance Spot universe.

Outcome Summary

The hypothesis reported a net Sharpe of 0.74, 13.4% annualized return, -33.6% maximum drawdown, 430 entries, and a 1.37% mean per-trade return. No pipeline backtest or optimization report was recorded.

Iteration History

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox economics are healthy and internally consistent, so profitability is NOT the reason for this fail. avg_trade_return_pct 3.73% x avg_position_pct 8.25% x 95 trades = ~29% of equity vs total_return 32.07% — the ledger reconciles to the equity curve (no accounting defect), and 3.73%/trade clears the 0.20% BINANCE_SPOT round trip by ~18x with commissions at only 2.64% of gross. Two things for the analyst to carry forward: (1) the reported Sharpe 0.547 is not internally consistent with CAGR 19.26% on annualized_volatility 8.12% — the daily MTM series moved on only 580 of 2401 days while the book holds 5 spot legs continuously from ~Nov-2022, so the risk-adjusted numbers (Sharpe, Sortino, vol, VaR, Calmar) are diluted/sparse relative to the realized ledger. State the symptom, assert no cause. (2) regime attribution already shows calm +42.6% (Sharpe 1.40) vs normal -4.1% and stressed -3.7%, i.e. the low-vol premium here is a calm-regime phenomenon in this sample — but those terciles are computed over a window that is majority-flat (see the critical issue), so re-read them after the window is fixed rather than acting on them now. - [critical] UNIVERSE MAKES 58% OF THE BACKTEST WINDOW STRUCTURALLY UNTRADEABLE, CORRUPTING EVERY RISK METRIC. The base template's cross-leg alignment barrier (base_template.py _maybe_process_primary + _sync_extra_iids) defers the primary bar until EVERY same-timeframe extra leg has a bar at that exact timestamp, and this strategy additionally requires a complete row in _row_for(). I verified first-bar dates in data/catalog for all 25 legs: the binding names are APTUSDT.BINANCE_SPOT (first 1-DAY bar 2022-10-20) and ICPUSDT.BINANCE_SPOT (2021-05-12). The primary BTCUSDT.BINANCE_SPOT starts 2017-08-18, and the full backtest passes no start date (backtest_agent._run_backtest sends only `end`), so the engine runs 2017-08 -> 2026-09 (~9.1y) while the book cannot place its first order until ~2022-11-10 (APT listing + 21 aligned rows of warmup). ~5.25 of 9.1 years (58%) mark as flat 0.00% days on the daily grid. The sandbox already demonstrates this exactly: stress windows covid_crash_2020, china_ban_2021, luna_collapse_2022 and rate_shock_2022 (Jan-Oct) all return precisely 0.0000%, while ftx_collapse_2022 (starting 2022-11-06) is the first non-zero one at -8.20%. Consequences, none of which the optimizer or analyst can undo: (a) Phase-2's objective is Sharpe computed on a series that is majority zeros — the sandbox reports 0.547 while the same book's CAGR over its ACTIVE span is 19.26%; (b) roughly five calendar years will report exactly 0.00% and count as non-positive against the positive-years-share validity floor; (c) the hypothesis's own pre-registered claims — Sharpe +0.74 over 2017-2026, 6 of 9 years positive, 'survives BOTH full bear markets' (2018 -19%, 2022 -26%) — cannot be produced at all, because 2018 and the first ten months of 2022 are inside the dead window; (d) the realized-vol regime terciles are computed over a series whose majority is dead days. This is a sizing/configuration defect with an obvious fix, not a statement about the edge: the trading logic itself is correct (see notes). (line 49) — fix: Pick a universe whose members all exist over the window you intend to measure, and say in the config rationale which window that is. Verified catalog first-bar dates for the current 25: APT 2022-10-20, ICP 2021-05-12, then FIL 2020-10-16 / AAVE 2020-10-16 / NEAR 2020-10-15 / AVAX 2020-09-23 / UNI 2020-09-18 / DOT 2020-08-19 / SOL 2020-08-12, then BCH 2019-11-29 / HBAR 2019-09-30 / DOGE 2019-07-06 / ALGO 2019-06-23 / ATOM 2019-04-30 / LINK 2019-01-17, then VET 2018-07-26 / ETC 2018-06-13 / TRX 2018-06-12 / XLM 2018-06-01 / XRP 2018-05-05 / ADA 2018-04-18, and BTC/ETH 2017-08-18, BNB 2017-11-07, - [warning] Buy sizing at a rotation is capped by `available = get_account_equity() * cash_buffer`, read AFTER the exit and trim SELLs are submitted on lines 279-298. In the backtest this is correct because NT's SimulatedExchange fills market orders inside the same engine iteration, so the USDT balance already reflects the sales. In paper/live it is not: market orders rest until the next tick, so on a rotation that sells 2-3 names to fund 2-3 new ones the strategy will see pre-sale cash, scale every deficit by `ratio < 1`, and under-deploy. Because held names are only resized when they drift outside the +/-25% tolerance band, the shortfall may persist until a later rotation. Backtest/paper divergence, not a backtest correctness bug. (line 309) — fix: Either compute `available` as (cash + notional of the exits/trims just submitted), or defer the buy leg of a rotation to the next bar after the sells have settled (a one-bar-deferred buy queue keyed on the rebalance period). If left as-is, say so explicitly in the rationale so the paper-parity check does not read the divergence as a defect. - [warning] Single-leg data outages halt the whole book. `_row_for()` returns None unless EVERY one of the 25 legs has a bar carrying exactly the primary bar's timestamp, and the base template's barrier independently blocks the primary bar in the same situation. One missing daily bar on any single name (collection gap, exchange halt) silently skips that day's row, leaving a hole in the log-return window used for realized vol; a name that stops publishing entirely (delisting) freezes the strategy forever with positions open. The behaviour is conservative rather than wrong, but it is invisible — nothing logs or counts the skipped days. (line 88) — fix: Count skipped rows (a `self._skipped_rows` int is auto-persisted by the base template) and log a warning when a leg is absent for more than ~2 consecutive primary bars, so a delisting shows up as a diagnostic instead of a silent flat book.

Iteration History

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] The smoke backtest is profitable, but performance is not used as a correctness verdict. - [critical] The hypothesis requires a fixed 25-coin Binance Spot universe, but the implementation configures only 14 instruments. Holding 5 names therefore selects 5/14 rather than 5/25, materially changing the cross-sectional strategy. (line 45) — fix: Restore the exact 25-name universe in extra_instruments and extra_bar_types, then rerun verification and backtesting. Alternatively, formally revise the hypothesis to the 14-name variant. - [critical] The hypothesis permits maximum gross exposure of 1.0x, but the implementation sets max_gross_exposure to 0.95 and clamps it at 0.95. (line 29) — fix: Use a 1.0 maximum or explicitly amend the hypothesis to pre-register the 0.95 cap.

Abandon Reason

verification_loop: Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['n_hold', 'cash_buffer', 'min_notional', '_param_bounds', 'rebalance_days', 'rv_lookback_days', 'target_annual_vol', 'max_gross_exposure', 'rebalance_tolerance'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - steady_downtrend: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - flat_ranging: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - volatility_spike: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - zero_volume: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000) - price_gap: TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float' (bar timestamp: 1735692060000)
Strategy report

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