Skip to content
All strategies

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

Original Research Lead hypothesis, after the published 'VRP' timing rule for volatility ETPs (Tony Cooper, 'Easy Volatility Investing', SSRN 2013). Short-volatility ETPs earn the VIX-futures roll-down plus the equity variance risk premium: implied volatility (VIX) usually sits above the volatility that is later realized, and SVXY collects that gap by being short front VIX futures. The premium is not constant. When realized volatility catches up with or exceeds VIX, the insurance seller is no longer paid for the……Show moreShow less

Original Research Lead hypothesis, after the published 'VRP' timing rule for volatility ETPs (Tony Cooper, 'Easy Volatility Investing', SSRN 2013). Short-volatility ETPs earn the VIX-futures roll-down plus the equity variance risk premium: implied volatility (VIX) usually sits above the volatility that is later realized, and SVXY collects that gap by being short front VIX futures. The premium is not constant. When realized volatility catches up with or exceeds VIX, the insurance seller is no longer paid for the risk, the futures curve tends to flatten or invert, and the roll-down turns into a loss. These are the episodes in which SVXY's large losses happen. The rule holds SVXY only while the premium is positive and stands in cash otherwise. Instruments: SVXY.USEQ (traded), SPY.USEQ (signal only: realized volatility), and the VIX index close from supplementary `global_benchmarks` (key VIX, daily). Signal at the close of session t, using data through session t-1 only (VIX's 16:15 ET settlement is later than SPY's 16:00 close, so same-day VIX would leak 15 minutes; the whole signal is lagged one session): VRP(t-1) = VIX_close(t-1) - 100 x sqrt(252) x stdev of the last rv_window SPY daily log close-to-close returns ending at t-1. Execution is next_open_fill (supported on USEQ): a decision at session t's close fills at session t+1's open. The strategy trades only from 2018-03-01. SVXY switched from -1x to -0.5x daily exposure on 2018-02-28, after the Feb-2018 'Volmageddon' loss of ~90%, so the earlier history comes from a different product. 2011-2018 bars serve only as warm-up. Pre-study (fixed rule, rv_window 10, threshold 0, one-session lag, open-to-open fills, 30% allocation, 2018-03 to 2026-10): 83 entries, average trade +1.76%, in the market 83% of sessions, Sharpe 0.62, max drawdown 13%. Buy-and-hold SVXY at the same 30% allocation over the same period: Sharpe 0.49, max drawdown 25%. So the gate's pre-registered incremental value (L217) is about +0.13 Sharpe and about half the drawdown. It is not a large return improvement. The pre-study varied the window: 5 sessions gave Sharpe 0.37-0.47, 21 gave 0.35-0.54. An added VIX-below-10-day-mean trend gate doubled turnover and cut the average trade to 0.1-0.4%, so it is excluded. Fee arithmetic: USEQ has zero commission and ~0.02-0.05% round trip (spread plus impact; SVXY trades millions of shares a day). The average trade of ~1.5-1.8% is about 30x cost. Worst open-to-open SVXY session since 2018: -27.8% (2024-08-02 to 08-05). At a 40% allocation that costs 11% of equity, inside the 15% max-trade-loss budget. Risk route (b): signal exit with capped, unlevered sizing (40% of equity, no leverage). Long-only on purpose. Shorting SVXY or buying UVXY in the off state would hold the negative-carry side (UVXY decays about 50-80% a year) and is unmodeled for borrow; the off state is cash. Rationale for the buckets: USEQ is 9.8% of the corpus, and the equity volatility-carry mechanism is untested in the factory. The one prior SVXY run was a calendar pre-VIX-settlement window that ended overfit. This is a regime gate on the premium itself, not a calendar window. Long-only is over-represented; it is justified here because the only tradable side with positive carry is long SVXY.

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

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] None from correctness. The pre-study gives the gate only about +0.13 Sharpe and roughly half the drawdown over buy-and-hold SVXY. When the analyst reviews the backtest, compare it with buy-and-hold at the same 40% allocation ( incremental value), not with zero. - [critical] 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……Show moreShow less

Verification failed (Layer 4 — QA review) [class=code_defect]: - [edge_concern] None from correctness. The pre-study gives the gate only about +0.13 Sharpe and roughly half the drawdown over buy-and-hold SVXY. When the analyst reviews the backtest, compare it with buy-and-hold at the same 40% allocation ( incremental value), not with zero. - [critical] 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. (line 102) — fix: 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. - [warning] 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). (line 67) — fix: Return self.rv_window + self.signal_lag_sessions + 1, or remove the override.

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.