Strategies
Equity Variance-Risk-Premium Gate on the Short-VIX ETF: hold SVXY (-0.5x short VIX futures) only while VIX exceeds SPY's trailing 10-session realized vol, flat otherwise (USEQ 1-DAY, next-open fills, 40% fixed allocation, trades only in the post-2018 -0.5x regime)
Outcome: Analyzing
SvxyVrpGateLong
The trades match the stated rule: 56 trades, all long SVXY, none before 2018-03-01, positions about 0.39-0.40x of equity, average hold about 29 days.
Hypothesis
Implementation
Long SVXY.USEQ (40% of equity, whole shares, 1x) only while the lagged variance risk premium VRP = VIX(t-1) - 100*sqrt(252)*stdev(last rv_window SPY daily log returns ending t-1) exceeds vrp_threshold; flat (cash) otherwise. Daily bars; a decision at session t's close fills at session t+1's open (platform next_open_fill). Entries only from 2018-03-01 (post -0.5x SVXY regime).
Iteration 2 fixes the QA critical: VIX is now read with the literal key supp_as_of("US_VIXCLS") / supp_window("US_VIXCLS", lag), so the platform's referenced-series scan detects and injects the FRED VIX series (checked locally: referenced_supp_series and referenced_macro_series both return ['US_VIXCLS']). Before, the key came from a variable, so the series was never loaded and the strategy could not trade. The now-unused vix_key parameter is removed. min_bars_required() now returns rv_window + signal_lag_sessions + 1, the real warm-up. Signal logic, sizing and the other parameters are unchanged. Risk route (b): signal exit plus capped unlevered sizing at 40% of equity. For the analyst, per QA: the gate should be judged against buy-and-hold SVXY at the same 40% allocation (pre-registered increment of about +0.13 Sharpe and about half the drawdown).
Verification Results
None required. You could say in the description that FRED VIXCLS stands in for the global_benchmarks VIX key.
The hypothesis names the VIX close from global_benchmarks (key VIX). The code reads FRED US_VIXCLS instead. Both are the CBOE VIX daily close. FRED stamps each value at 00:00 UTC the day after its date, so at session t's close supp_as_of returns VIX(t-1). That is exactly the one-session lag the hypothesis specifies, and it is not a mismatch.
Optionally treat VIX as invalid (go flat) when the latest observation is more than about 5 calendar days older than the bar.
If the FRED feed stops updating, supp_as_of keeps returning the last published VIX with no limit on how old it is, so the gate would run on a stale VIX. This does not affect the backtest, where the history is continuous. It matters for paper and live.
None required.
min_bars_required() overrides the base method, which Layer 1 flagged. The override is deliberate: it returns rv_window + lag + 1 instead of the base default of 20. The SPY buffer cap (_max_bars >= rv_window + lag + 30, base 500) easily holds the 22 closes needed.
Judge the gate against buy-and-hold SVXY at the same 40% allocation. The pre-registered increment is small: about +0.13 Sharpe and about half the drawdown. Tail risk: one open-to-open session (2024-08-02 to 08-05, -27.8%) costs about 11% of equity at 40%. The hypothesis says this fits the 15% max-trade-loss budget, but a 1-session-lagged gate cannot step aside before an overnight VIX spike.
Backtest Review
- Sharpe
- 0.44
- Total return
- 73.05%
- Max drawdown
- 17.00%
- Trades
- 83
- Win rate
- 60.2%
- Profit factor
- 1.76
The trades match the stated rule: 56 trades, all long SVXY, none before 2018-03-01, positions about 0.39-0.40x of equity, average hold about 29 days.
Average trade 2.01% against the 0.05% USEQ floor; PF 2.12; win rate 61%.
2018-03 to 2023-10: about +48% with a 16.9% max drawdown, inside the 20% limit. A crude 40%-scaled replay of the report's buy-and-hold curve gives about +28% with an 18% drawdown; I have not confirmed which instrument that curve tracks.
The signal is point-in-time (one-session lag) and orders fill at the next open.
Worst trade -$12.1k, about 10-11% of equity, inside route (b) of the risk-control rule.
Headline Sharpe 0.44 and Calmar 0.20 are diluted by 2011-2018 warm-up years with no trades.
56 trades, about 10 a year, so the walk-forward windows will be long and few.
Tail risk is real (2020-02 cost -9.9%); the lagged gate cannot step aside before an overnight VIX spike.
Iteration History
Read VIX by a literal key, e.g. `v = self.supp_as_of("US_VIXCLS")`, and for lag > 1 `w = self.supp_window("US_VIXCLS", lag)`. Drop the vix_key parameter, or keep it only as a check that it equals the literal. Then confirm the Layer 2 signals are no longer frozen and the sandbox trades.
The strategy never receives the VIX series, so it can never trade. The platform loads a FRED macro series into a backtest only when the code reads it by a literal name. src/backtesting/macro_supp.referenced_macro_series → supp_collector.referenced_supp_series matches patterns like `supp_as_of("US_VIXCLS")`. This code calls `self.supp_as_of(self.vix_key)` (and `self.supp_window(self.vix_key, lag)`) with a variable, so the regex finds nothing. I ran it on this file: referenced_supp_series → [] and referenced_macro_series → []. With the literal appended it returns ['US_VIXCLS']. As a result US_VIXCLS is never injected, _vix() returns None on every bar, signal_valid is always False, and the strategy stays flat. The evidence agrees. Every synthetic scenario produced a frozen signal of 0.0 (Layer 2 injects synthetic macro values only for detected keys). The Layer 3 sandbox made zero trades over 1,621 sessions from 2020-03, a period in which the pre-study had the gate in the market about 83% of sessions. The full backtest would also make zero trades.
Return self.rv_window + self.signal_lag_sessions + 1, or remove the override.
min_bars_required() is overridden to return 2, which shadows the base method. This is harmless: the signal is invalid (flat) until rv_window+1 SPY closes and a VIX value exist, and trading begins only in 2018, after years of warm-up. Even so, the readiness gate does not reflect the real warm-up (rv_window+1+lag).
Backtest and paper results are hypothetical. Trading involves risk of loss.