Skip to content

View original

BtcPositioningCrowdednessReversalOiFundingJointZ

Hypotheses

BTC Positioning Crowdedness Reversal: Counter-Trade BTCUSDT.BINANCE 1H When Open Interest + Funding Joint Z-Score Exceeds ±2

Hypotheses

A long-short mean-reversion strategy on BTCUSDT.BINANCE USD-M perpetual futures that uses ONLY supplementary derivatives data (open interest + funding rate) as the entry signal — NO OHLCV pattern recognition, NO moving averages, NO breakouts, NO 'practitioner pattern' that has failed 10+ times this session. The hypothesis: when both open interest is unusually elevated (z-score > +2 over rolling 30 days) AND funding rate is unusually positive (z-score > +2 over rolling 30 days), the market is in a CROWDED-LONG state where leveraged longs are paying premium funding to maintain positioning that has just been built — historically this configuration mean-reverts as the marginal buyer is exhausted and overpositioned longs become forced sellers. SYMMETRIC reverse: crowded-short state (both z-scores ≤ -2) tends to revert upward. The conjunction requirement (BOTH signals must agree) acts as a structural overfitting suppressor — a single z-threshold has many ways to curve-fit, but a joint z-threshold on TWO INDEPENDENT data sources has far fewer because both surfaces must align in extreme tails simultaneously. Fills FOUR under-represented buckets: long-short direction (14.2% → toward 45%), non-OHLCV-mechanism class (zero of failures used joint positioning signals), mid-term hold (24h cycles), and supplementary-data-driven (this taps a data stream — OI + funding — that BINANCE has consistently clean coverage on, distinct from spot bars which have failed Layer 3 twice). Uses ONLY BINANCE USD-M perp bars + Postgres-stored OI and funding rate data (no BINANCE_SPOT — avoiding the recent failure mode). Position sizing: 15% of equity per direction, single-leg directional trade. Risk per trade: 1.5% of equity capped by stop-loss.

Hypotheses

Iteration 5 fixes ONLY the reported Layer-3 sandbox_timeout, with no change to signal logic, thresholds, imports, config or sizing (all of which already passed Layers 1-2 and reached Layer 3). The timeout came from _zscore(), which recomputed sum(buf) and sum((x-mean)**2 for x in buf) over a 721-element deque for BOTH series on EVERY one of ~20,000 bars (~30M interpreted float ops). It is replaced by _RollingZ, an O(1)-per-bar accumulator that keeps running sum and sum-of-squares over the identical fixed window, mean-shifted by the first observation for numerical conditioning, with an exact recompute every 4096 pushes so float drift cannot accumulate. Offline check against the old brute-force z over 9,000 pushes at both OI-scale (1e6) and funding-scale (1e-4) magnitudes shows max absolute z difference 1.8e-14, i.e. numerically identical z-scores, and 20,000 push+read now costs 0.09s instead of tens of seconds. Supplementary lookups were already O(log n) via bisect over arrays built once in _ensure_supp, so no other per-bar work scales with history.

Hypotheses

verification_loop: sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (833 days of BTCUSDT.BINANCE-1-HOUR-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

Long-short mean reversion on BTCUSDT.BINANCE USD-M perp 1H bars driven purely by derivatives positioning data. Rolling ~30-day z-scores of open interest (sumOpenInterest) and funding rate are computed incrementally each bar; when BOTH exceed +entry_z (crowded long) the strategy shorts, when BOTH fall below -entry_z (crowded short) it goes long. Exits on 4% stop, 5% take-profit, 48-bar time stop, or when the crowding relaxes back inside +/- exit_z. Sizing is 15% of equity notional, capped so stop-loss risk stays <= 1.5% of equity. No OHLCV pattern drives entries and there is no price-only fallback when supplementary data is missing (it simply stays flat).

Verification Results

sandbox_timeout: Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit on a ~20000-bar window (833 days of BTCUSDT.BINANCE-1-HOUR-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 2 — synthetic scenarios): Parameters used: ['z_min', 'exit_z', 'tp_pct', 'entry_z', 'risk_pct', 'stop_pct', 'window_bars', 'min_notional', 'notional_pct', 'max_hold_bars', 'oi_tolerance_s', 'funding_tolerance_s'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000) - steady_downtrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000) - flat_ranging: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000) - volatility_spike: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000) - zero_volume: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000) - price_gap: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735689840000)

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['z_min', 'exit_z', 'tp_pct', 'entry_z', 'risk_pct', 'stop_pct', 'window_bars', 'min_notional', 'notional_pct', 'max_hold_bars', 'oi_tolerance_s', 'funding_tolerance_s'] Check that __init__ sets all attributes from self.parameters.get(). - steady_uptrend: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000) - steady_downtrend: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000) - flat_ranging: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000) - volatility_spike: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000) - zero_volume: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000) - price_gap: TypeError: BtcPositioningCrowdednessReversalOiFundingJointZ._nearest() takes 3 positional arguments but 4 were given (bar timestamp: 1735689840000)

Iteration History

Verification failed (Layer 3 — sandbox backtest): No trades produced Bar type used: BTCUSDT.BINANCE-1-HOUR-LAST-EXTERNAL, Bars processed: 56856 Diagnostics: should_enter() returned a side 0 times over 56852 evaluated bars -> your ENTRY CONDITION never triggered. Loosen the entry logic / thresholds. Ensure your strategy produces trades with the given data and parameters.

Iteration History

Verification failed (Layer 1 — static analysis): - Look-ahead: a supplementary observation is selected by ABSOLUTE distance to the bar timestamp, which returns the NEXT observation when it is closer than the previous one. Use self.supp_as_of(key) (latest at or before the bar) or an explicit `ts <= bar.ts_init` filter — never abs(). (line None)
Strategy report

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