Skip to content

View translation

EthDeribitWeeklyPutCreditSpreadSkewVrp

Hypotheses

ETH Deribit Weekly Put-Credit-Spread — Volatility-Risk-Premium + Downside-Skew Harvest (Defined-Risk Options, Skew-Gated, 3-Parameter)

Hypotheses

A DEFINED-RISK, single-venue options strategy on DERIBIT ETH weekly expiries that sells the persistently over-priced downside of the crypto vol surface. Once per weekly cycle (front expiry ~5-7 DTE), and ONLY when downside insurance is demonstrably expensive (25-delta put IV richer than ATM IV by a skew margin AND ATM IV above trailing realized vol), it opens a PUT CREDIT SPREAD: SELL one ~25-delta put and BUY one ~10-delta put of the same expiry. Max loss is capped at (strike width − net credit), so the account cannot be liquidated by a tail move — directly addressing the biggest risk-review concern with short-vol strategies. This deliberately fills the factory's single largest portfolio gap: options are 4.7% of the book vs a ≥15% quota, with essentially zero prior coverage. It is intentionally distinct from a plain short strangle — different underlying (ETH), different structure (defined-risk vertical, not naked two-sided), and a different signal (put-SKEW richness + VRP, not raw IV/RV). Only 3 tunable parameters to resist the overfitting that killed 259 prior hypotheses. NOT funding-carry (L33), NOT a momentum/trend-confluence port (exhausted), NOT a price reversal/fade — the P&L source is a structural insurance premium plus the crypto put-skew premium, not a directional price forecast.

Hypotheses

The Layer-3 'No trades produced / 0 evaluated bars' had two concrete causes, and neither was the entry threshold (the feedback correctly says not to loosen thresholds). First, the '0 evaluated bars' line is a red herring for this family: RollingOptionsStrategy.on_bar fully overrides FactoryStrategy.on_bar and never reaches _process_primary, so bars_seen is 0 for every rolling-options strategy including ones that trade normally — I confirmed this by reading rolling_options_template.py and base_template.py rather than adding a super().on_bar() call that would have routed primary bars into the base's cross-leg alignment barrier, where the intermittent option legs (same 1-DAY timeframe as the primary) would have blocked processing forever. Second, the actual blockers: (a) ANNUALIZATION MISMATCH — the inherited realized-vol settings were hourly (rv_window=168, bars_per_year=8760) while the run is on 1-DAY bars, so daily log returns were annualized with sqrt(8760) instead of sqrt(365), inflating RV by 4.90x (a normal 3%/day ETH tape reads 281% instead of 57%); the VRP gate iv >= rv*(1+margin) then compared ~60% IV against ~281% RV and was False on every cycle. Fixed by deriving bars_per_year from the configured bar type and expressing the RV window in calendar days, so the drift cannot recur on any interval (verified: 1-DAY -> 365, 1-HOUR -> 8760, 4-HOUR -> 2190, and the gate now passes on a normal tape). (b) ROLE MAPPING — the inherited scaffolding keyed legs by option kind and required one call plus one put (a strangle), so a put spread's two P legs collided on one key, the second was rejected as a duplicate, the readiness check demanded a nonexistent call leg, and every leg was submitted SELL (two naked short puts, not a defined-risk vertical). Legs are now mapped by strike into short/long, the short is sold and the long bought at the same cached contract count, atomically. I also removed the inherited premium-multiple stop: the long wing already bounds the loss, so a stop on a capped structure only realizes a bounded loss early and adds fees. Sizing arithmetic verified end-to-end (150-wide spread, $22 credit, $128 max loss -> 15 contracts = 1.9% of equity risked). I kept everything that passed Layers 1-2 intact: clean imports, the module-level _bar_ts free function that fixes the Layer-2 staticmethod rebinding TypeError, the DVOL parser, the Black-Scholes IV inversion, and the continuous calculate_signal. lookback_days is 49 (7 cycles, 14 option legs) because the per-bar-type catalog loader costs ~10s per leg and a larger chain risks the Layer-3 wall clock; that caps the initial sample and is a loader property, not a hypothesis property.

Hypotheses

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox avg_trade_return_pct is -45.45% on 2 trades with metrics_reliable=false and data_days=1 — this is not a performance verdict (per-notional return is not meaningful for option legs, where premium notional << the wing notional, and the sandbox is a 1-day smoke test). It is carried forward only as context: if the loader/lookback issue below is resolved and a real weekly-options sample accrues, the analyst should re-check whether the net credit collected clears Deribit's 0.03%/side round-trip across 4 legs (open+close x short+long) plus any assignment slippage, since put-credit-spread per-notional edge is structurally thin. - [critical] rolling_options.lookback_days=49 confines the ENTIRE resolvable option chain of ANY backtest to the ~49 days ending 'now' = ~7 weekly cycles / ~14 option legs, regardless of Deribit's 6.5-year ETH option history. The strategy sits flat for the rest of the window and only trades at the tail. The sandbox confirms this empirically: bars_processed=2400 (~6.5 years) yet data_days=1 and total_trades=2 (trades_count=4). A variance-risk-premium + skew edge cannot be distinguished from noise on ~7 cycles / ~14 legs — far below the ~30-100 trades needed for a measurable options sample. This is the exact loader-cost-vs-sample-size catch-22 that caused the near-identical BtcDeribitWeeklyDeltaHedgedAtmStraddleVrp (same venue, same lookback_days=49) to be abandoned at Layer 4 days ago. The developer's own rationale acknowledges the cap ('lookback_days is 49 (7 cycles, 14 option legs) because the per-bar-type catalog loader costs ~10s per leg and a larger chain risks the Layer-3 wall clock'). As configured, the strategy can never produce a measurable full backtest. — fix: Do not pass on ~7 cycles. Resolve the loader-cost vs sample-size catch-22 before re-submitting: batch/parallelize or cache the per-leg catalog.bars() loads across bars so lookback_days can reach many years of weekly cycles (~200-300 cycles) without breaching the sandbox wall-clock, then set lookback_days to cover the full test window. If the loader genuinely cannot resolve a measurable weekly-options history within the time budget, this family is not testable in the current harness at weekly cadence and should be redesigned (e.g. fewer, longer-dated monthly cycles across the full span) rather

Implementation

Defined-risk weekly put credit spread on Deribit ETH. Each weekly expiry cycle (~7 DTE) the rolling-options framework resolves two put legs (~5% and ~10% OTM); the strategy maps them by strike into a short (higher-strike, ~25-delta) and long (lower-strike, ~10-delta) leg, SELLS the short and BUYS the long at an identical 1:1 contract count, and holds to the roll buffer before European cash settlement. Entry is gated on two richness tests measured at the cycle's entry window: downside skew (short-put IV >= Deribit DVOL ATM IV x (1 + skew_margin)) and variance risk premium (ATM IV >= trailing 14-day realized vol x (1 + vrp_margin)). Leg IVs are inverted from each leg's own USD-premium bar with Black-Scholes. Max loss per spread is (strike width - net credit), fixed at entry, and sizing risks exactly that: contracts = floor(equity x risk_frac / max_loss), capped so gross spread width stays inside 50% of equity and floored at Deribit's 1-contract ETH minimum. 3 tunables (skew_margin, vrp_margin, risk_frac); leverage 1.0; no perp/underlying trading path.

Verification Results

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox avg_trade_return_pct is -45.45% on 2 trades with metrics_reliable=false and data_days=1 — this is not a performance verdict (per-notional return is not meaningful for option legs, where premium notional << the wing notional, and the sandbox is a 1-day smoke test). It is carried forward only as context: if the loader/lookback issue below is resolved and a real weekly-options sample accrues, the analyst should re-check whether the net credit collected clears Deribit's 0.03%/side round-trip across 4 legs (open+close x short+long) plus any assignment slippage, since put-credit-spread per-notional edge is structurally thin. - [critical] rolling_options.lookback_days=49 confines the ENTIRE resolvable option chain of ANY backtest to the ~49 days ending 'now' = ~7 weekly cycles / ~14 option legs, regardless of Deribit's 6.5-year ETH option history. The strategy sits flat for the rest of the window and only trades at the tail. The sandbox confirms this empirically: bars_processed=2400 (~6.5 years) yet data_days=1 and total_trades=2 (trades_count=4). A variance-risk-premium + skew edge cannot be distinguished from noise on ~7 cycles / ~14 legs — far below the ~30-100 trades needed for a measurable options sample. This is the exact loader-cost-vs-sample-size catch-22 that caused the near-identical BtcDeribitWeeklyDeltaHedgedAtmStraddleVrp (same venue, same lookback_days=49) to be abandoned at Layer 4 days ago. The developer's own rationale acknowledges the cap ('lookback_days is 49 (7 cycles, 14 option legs) because the per-bar-type catalog loader costs ~10s per leg and a larger chain risks the Layer-3 wall clock'). As configured, the strategy can never produce a measurable full backtest. — fix: Do not pass on ~7 cycles. Resolve the loader-cost vs sample-size catch-22 before re-submitting: batch/parallelize or cache the per-leg catalog.bars() loads across bars so lookback_days can reach many years of weekly cycles (~200-300 cycles) without breaching the sandbox wall-clock, then set lookback_days to cover the full test window. If the loader genuinely cannot resolve a measurable weekly-options history within the time budget, this family is not testable in the current harness at weekly cadence and should be redesigned (e.g. fewer, longer-dated monthly cycles across the full span) rather

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['contracts', 'leg_sides', 'risk_frac', 'rv_window', 'atm_offset', 'skew_margin', 'iv_rv_margin', 'bars_per_year', 'min_contracts', 'iv_stale_hours', 'rv_slow_window', 'long_put_offset', 'rolling_options', 'short_put_offset', 'max_notional_frac', 'min_bars_required', 'roll_buffer_hours', 'entry_window_hours'] 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: 1735691040000) - steady_downtrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - flat_ranging: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - volatility_spike: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - zero_volume: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - price_gap: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000)

Iteration History

BtcDeribitWeeklyDeltaNeutralShortStrangleVrp

Iteration History

Verification failed (Layer 2 — synthetic scenarios): Parameters used: ['contracts', 'leg_sides', 'risk_frac', 'rv_window', 'stop_mult', 'put_offset', 'call_offset', 'iv_rv_margin', 'bars_per_year', 'min_contracts', 'iv_stale_hours', 'rv_slow_window', 'max_delta_ratio', 'rolling_options', 'max_notional_frac', 'min_bars_required', 'roll_buffer_hours', 'entry_window_hours'] 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: 1735691040000) - steady_downtrend: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - flat_ranging: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - volatility_spike: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - zero_volume: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000) - price_gap: TypeError: FactoryStrategy._bar_ts() takes 1 positional argument but 2 were given (bar timestamp: 1735691040000)

Iteration History

Verification failed (Layer 3 — sandbox backtest): No trades produced Bar type used: ETH-PERPETUAL.DERIBIT-1-DAY-LAST-EXTERNAL, Bars processed: 2400 Diagnostics: calculate_signal() was never evaluated (0 evaluated bars) -> no primary bar reached the base template's signal path -> if you override on_bar(), call super().on_bar(bar); check bar_type matches the instrument's data. Do NOT loosen entry thresholds — the entry logic never ran. Ensure your strategy produces trades with the given data and parameters.

Iteration History

Verification failed (Layer 4 — QA review) [class=hypothesis_mismatch]: - [edge_concern] Not the basis for the verdict — the block is fidelity, not performance. (For the record the sandbox is also negative: -2.9%, PF 0.13, win 0.125, a -$2,149 tail on a supposedly stopped book — but that's the wrong strategy's result.) - WHOLE-STRATEGY SWAP / MISLINK. Hypothesis is an ETH Deribit weekly PUT CREDIT SPREAD (sell ~25d put, BUY ~10d put — defined-risk vertical). Code is the BTC Deribit weekly delta-neutral SHORT STRANGLE (class name, docstring, iter-3 rationale, and every mechanism are the strangle). Instrument AND mechanism both differ → mis-generated/mislinked artifact, not this hypothesis. Re-generate/re-link the correct ETH put-credit-spread. - Wrong instrument: hypothesis is ETH, but instrument_id and bar_type are BTC-PERPETUAL.DERIBIT and rolling_options.underlying is 'BTC'. No ETH anywhere. - Wrong structure / not defined-risk: hypothesis caps max loss with a PURCHASED ~10d put wing. Code submits TWO SELL legs with NO bought wing; tail bounded only by a soft next-bar premium stop (sandbox largest_loss -$2,149 vs avg_win ~$214 shows it doesn't cap the tail) — exactly the uncapped profile the hypothesis avoids. - Core edge absent: hypothesis signal is downside-SKEW richness (25d-put IV vs ATM IV) plus IV>RV. Code computes no skew — only mean-strangle IV/RV VRP. The put-skew harvest that distinguishes this hypothesis is not implemented.

Abandon Reason

verification_loop: Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] Sandbox avg_trade_return_pct is -45.45% on 2 trades with metrics_reliable=false and data_days=1 — this is not a performance verdict (per-notional return is not meaningful for option legs, where premium notional << the wing notional, and the sandbox is a 1-day smoke test). It is carried forward only as context: if the loader/lookback issue below is resolved and a real weekly-options sample accrues, the analyst should re-check whether the net credit collected clears Deribit's 0.03%/side round-trip across 4 legs (open+close x short+long) plus any assignment slippage, since put-credit-spread per-notional edge is structurally thin. - [critical] rolling_options.lookback_days=49 confines the ENTIRE resolvable option chain of ANY backtest to the ~49 days ending 'now' = ~7 weekly cycles / ~14 option legs, regardless of Deribit's 6.5-year ETH option history. The strategy sits flat for the rest of the window and only trades at the tail. The sandbox confirms this empirically: bars_processed=2400 (~6.5 years) yet data_days=1 and total_trades=2 (trades_count=4). A variance-risk-premium + skew edge cannot be distinguished from noise on ~7 cycles / ~14 legs — far below the ~30-100 trades needed for a measurable options sample. This is the exact loader-cost-vs-sample-size catch-22 that caused the near-identical BtcDeribitWeeklyDeltaHedgedAtmStraddleVrp (same venue, same lookback_days=49) to be abandoned at Layer 4 days ago. The developer's own rationale acknowledges the cap ('lookback_days is 49 (7 cycles, 14 option legs) because the per-bar-type catalog loader costs ~10s per leg and a larger chain risks the Layer-3 wall clock'). As configured, the strategy can never produce a measurable full backtest. — fix: Do not pass on ~7 cycles. Resolve the loader-cost vs sample-size catch-22 before re-submitting: batch/parallelize or cache the per-leg catalog.bars() loads across bars so lookback_days can reach many years of weekly cycles (~200-300 cycles) without breaching the sandbox wall-clock, then set lookback_days to cover the full test window. If the loader genuinely cannot resolve a measurable weekly-options history within the time budget, this family is not testable in the current harness at weekly cadence and should be redesigned (e.g. fewer, longer-dated monthly cycles across the full span) rather
Strategy report

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