Skip to content

View translation

BtcWeeklyIronFlyAtmVrpHarvest

Hypotheses

Binance Options BTC Weekly Iron Fly — ATM Short-Straddle VRP Harvest, IV-Rank Gated, Defined Risk (fills the empty BINANCE_OPTIONS venue)

Hypotheses

A DEFINED-RISK, market-neutral options premium-harvest on BINANCE_OPTIONS BTC — deliberately targeting the single EMPTIEST bucket in the entire 2271-experiment portfolio: BINANCE_OPTIONS at 1 experiment (0.0%), versus the ≥15% combined-options quota. It is distinct in venue, strike-selection, and regime-dependency from my two already-pending Deribit structures (an OTM iron condor and a directional bull-put skew spread on Deribit). This is an ATM IRON FLY on Binance options: each weekly cycle we SELL the ATM straddle (short ATM call + short ATM put) and BUY protective wings a fixed distance out (long OTM call + long OTM put), collecting a large ATM credit while capping max loss with the wings. Where an iron condor profits from price staying inside a WIDE band, an iron fly profits from realized vol staying LOW / price pinning near the entry spot — a genuinely different Greek and regime exposure, so it is NOT a re-skin of the queued condor. We only put the trade on when implied vol is genuinely rich (IV-rank / IV-minus-trailing-realized above a threshold), harvesting the persistent crypto Variance Risk Premium (IV structurally > subsequent RV). Defined risk via the long wings directly answers the risk_reject / 52–74%-DD failures (L48). This is NOT the exhausted 'gate a 4H price move with a supplementary feed' template (L46): the payoff is premium decay (a cash flow of several % of notional per ATM structure), not a predicted directional move, and it clears the ~0.06% Binance-options round-trip fee by one to two orders of magnitude. ~1 structure/week held toward expiry → negligible fee drag. Simple: 3 parameters (wing distance/DTE, IV-rank threshold, take-profit fraction) to resist the 300x overfit graveyard. BTC chosen (not an alt) because Binance BTC options have the longest, densest history — avoiding the L47 data-span abandon risk.

Hypotheses

ITERATION 3 — ONE CONFIG VALUE CHANGED (lookback_days 365 -> 45). Strategy logic is byte-for-byte identical; only the docstring gained a section explaining the constraint. THE FEEDBACK'S DIAGNOSIS DOES NOT FIT THIS STRATEGY, and I verified rather than assumed. There is no per-bar work scaling with history: the IV-rank lookup is two np.searchsorted calls into arrays built ONCE and cached per expiry after the first hit; the take-profit check iterates at most one open structure's four legs; there is no list rebuilt per bar and no sorted()/min() in the hot path. THE ACTUAL CAUSE IS THE ENGINE'S PER-INSTRUMENT EXTRAS LOADING, WHICH I MEASURED. src/backtesting/runner.py loads extras one at a time — `for ebt_str in extra_bar_types: self.catalog.bars(bar_types=[str(ebt)], start=start, end=end)`. On this catalog that costs 7.26s PER OPTION INSTRUMENT (20 instruments = 145.1s). The overhead is per-CALL, not per-bar — the same 20 instruments load in 7.8s (0.39s each, 18.6x faster) when passed to catalog.bars() in a SINGLE call — so a coarser leg timeframe would not have helped. An iron fly is FOUR legs per week, so 365 days = ~52 expiries x 4 = ~208 instruments = ~1510s, five times the 300s cap. THE FIX I APPLIED IS THE ONLY LEVER A STRATEGY HAS: leg count. 45 days gives ~6 weekly expiries x 4 legs = ~24 instruments (~174s), leaving room for the run. I DO NOT WANT THIS READ AS A SUCCESS, AND THE RESEARCH LEAD SHOULD DECIDE WHAT HAPPENS NEXT. With the IV-rank gate passing ~43% of weeks (measured: 121 of 279 historical weeks at rank >= 0.30), six structures yield roughly 2-3 actual trades. That is enough to prove the machinery runs and to clear the >=1-option-trade gate, but it is NOT enough to evaluate the edge — it will not support a walk-forward, a holdout, or any honest Sharpe. My sibling 2-leg ETH put spread squeezed into 90 days/26 legs and gets ~7 trades; a 4-leg structure cannot be squeezed to a useful sample at all under the current loader. So the real choice is binary: either land the one-line batched read in runner.py's extras loop — which would let this run the full 365-day chain (~208 legs in ~80s) and produce ~22 gated structures — or park this hypothesis until that lands. I would not spend further verification iterations on config shaving; there is no configuration that is both fast enough and statistically meaningful. That engine fix unblocks EVERY rolling-options strategy in the factory, which matters right now because the options bucket is deliberately being filled. UNCHANGED FROM ITERATION 1, and not re-litigated here: the venue override to Deribit (BINANCE_OPTIONS has zero catalog bar directories, a BNB-only 5-day mirror, and no chain resolver, so it would place zero option trades), the defined-risk wing validation that prevents an unpaired ATM short from ever being sold, and the measured model edge (+0.90% of spot per week held to expiry, t=+4.58 over 279 weeks; +1.24% at IV rank >= 0.30) with its honest caveat that a flat-DVOL model understates the true cost of the 10% wings and therefore overstates the net credit. NO REGRESSION: imports unchanged, no banned imports, class structure and the explicitly-declared inert FactoryStrategy hooks (which keep the underlying perp from ever being traded) are exactly as Layers 1 and 2 already passed them.

Hypotheses

data_wall: Not worth 2 hours of optimization: the edge cannot be tested at a useful sample size. Only 8 trades over an effective 5-day window (data_days=5, metrics_reliable=false, 'only 2 days moved the equity curve'); Sharpe 0.0 and every robustness statistic is null. The developer's own iteration-3 note explains the ceiling — the backtest engine loads each option instrument individually (~7.3s), so a 4-leg weekly structure is clamped to ~45 days ≈ 2-3 trades, which is 'enough to prove the machinery runs, NOT enough to evaluate the edge.' This is an engine per-instrument loading limit, not a strategy-parameter or premise flaw: the 3 tunable knobs (iv_rank_min/tp_frac/roll_buffer_hours) cannot raise the trade count, so iteration cannot help (L27: options results with <10 trades are unmeasured — abandon rather than iterate, since options cadence and the loader budget cap the count). Additionally the declared venue (BINANCE_OPTIONS) has no resolvable chain in the catalog, so the hypothesis's own venue is untestable; the run silently substitutes Deribit. Revive the same mechanism only once the engine can batch-load the option legs (the developer's one-line fix would return the window to ~365 days / ~208 legs) so a full walk-forward and holdout have an adequate sample. abandon_class=unmeasurable.

Implementation

Weekly defined-risk ATM iron fly on BTC options. Each weekly cycle the chain resolver supplies four front-week (7 DTE) legs: the ATM call and ATM put are SOLD (the short straddle, peak theta) and 10% OTM call and put are BOUGHT as wings, capping maximum loss at the 10% width less the credit. The structure is established only when implied vol is rich — DVOL's percentile rank within its own trailing 365-day history must be >= 0.30 — otherwise the week is skipped. It is closed early once 75% of the entry credit has been captured (measured from the legs' own live prices), or otherwise 2 hours before expiry. Leg roles come from strike order within each expiry, and a side is traded ONLY if both its legs resolved: an unpaired ATM short is dropped rather than sold naked. Sizing is one contract with a capital-relative floor (notional = max(1 BTC, equity x 0.5)). The rolling chain window is 45 days (~6 weekly structures) purely because the backtest engine's per-instrument catalog loading caps how many option legs fit inside the Layer-3 wall clock — a 4-leg structure cannot fit a useful sample at present. Three tunable parameters: iv_rank_min, tp_frac, roll_buffer_hours.

Verification Results

CLEAN RESTART 2026-09-04 — this run's verdict history and learning records were removed and it was restarted from verification. Its previous abandonment came from the pipeline, not from the market: the Layer-2 harness mis-bound @staticmethod helpers (fixed), QA issued terminal performance verdicts on an unoptimized smoke test (removed — QA now judges correctness only), and sandbox timeouts came from backtest-slot starvation (fixed). The hypothesis and the strategy code are unchanged. Verify the code on its merits; performance is decided later by the full backtest and the optimizer.

Backtest Review

Defined-risk structure (protective wings enforced in code, unpaired shorts dropped) directly answers the risk_reject / deep-drawdown failure class.

Backtest Review

Machinery runs: 8 option trades executed, iron-fly legs classified correctly, IV-rank gate and take-profit monitor fire; net_entry_theta positive / net_entry_vega negative confirm a genuine short-vol structure.

Backtest Review

Only 3 tunable parameters, so overfit surface is small in principle.

Backtest Review

Only 8 trades over an effectively 5-day window (data_days=5). metrics_reliable=false, sharpe_note flags 'only 2 days moved the equity curve'. Sharpe 0.0, and deflated_sharpe/PSR/CI/regime all null — there is nothing statistically meaningful to optimize.

Backtest Review

The developer's own iteration-3 note concedes the ceiling: the engine loads option instruments one-at-a-time (~7.3s each), so a 4-leg weekly structure caps at ~45 days ≈ 2-3 real trades. That is an ENGINE loader limit, not a strategy-parameter or premise issue — the 3 knobs cannot raise the trade count.

Backtest Review

Venue is a disclosed override: hypothesis names BINANCE_OPTIONS but the run is Deribit because no BINANCE_OPTIONS chain exists in the catalog (Binance mirror is 1,150 BNB-only rows over 5 days). The stated venue is untestable.

Backtest Review

avg_trade_return_pct 86.7% is against option premium notional (not fee-comparable), and on 8 trades it carries no statistical content (win_rate 0.5 from 8 trades is noise).

Iteration History

Verification failed (Layer 1 — static analysis): - Required method not implemented: should_enter (line None) - Required method not implemented: should_exit (line None) - Required method not implemented: position_size (line None) - Required method not implemented: calculate_signal (line None)

Iteration History

Verification failed (Layer 3 — sandbox backtest): smoke test exceeded the 300s wall-clock limit. 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).
Strategy report

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