TL;DR — On 5 February 2018, inverse-volatility ETPs lost ~$3 billion in 50 minutes. XIV — a Credit Suisse ETN — went from $108 to $4. It was not a freak event; it was the latest instance of a reflexive-hedging blow-up that happens every 3–4 years. A LASSO-regression stop — Example 8 from my book Hands-On AI Trading with Python, QuantConnect and AWS — would have flattened the position hours before the cliff. I re-ported the algorithm to C# on QuantConnect, ran three backtests side-by-side, and the equity curve tells the story.

1. Executive summary
- The product: XIV — VelocityShares Daily Inverse VIX Short-Term ETN, issued by Credit Suisse AG (Nassau branch), ticker XIV, CUSIP 22542D506. Launched 29 Nov 2010. Terminated 21 Feb 2018.
- The event: 5 Feb 2018. VIX +116%. XIV −96%. SVXY −91%. VMIN −87%. S&P 500 only −4.1%.
- The pattern: Reflexive-hedging blow-ups happen roughly every 3–4 years across different wrappers. Same machine; different paint job.
- The fix: Chapter 6, Example 8 — a LASSO-regression dynamic stop on {VIX, ATR(22), σ(22)} → weekly-low return.
- The backtest: Three QCAlgorithm classes — Buy-and-Hold, Fixed 5% stop, LASSO stop — over 1 Jan 2016 → 28 Feb 2018 on SVXY.
- The when-and-how: a decision table (Section 7) matches each algorithm to the regime, instrument, and portfolio role it is actually good at.
- The sequel: Volmageddon 2.0 is most likely priced in 0-DTE gamma.
Book — the full algorithm and 19 other worked examples: Hands-On AI Trading with Python, QuantConnect and AWS.
2. Quick primer: what is an ETP?
An ETP (Exchange-Traded Product) is an umbrella for three structurally different wrappers:
| Wrapper | Stands for | What it is | Credit risk? |
|---|---|---|---|
| ETF | Exchange-Traded Fund | Holds a basket of underlying assets. | Low — ring-fenced |
| ETN | Exchange-Traded Note | An unsecured debt promise from an issuing bank. | High — bank can “accelerate” |
| ETC | Exchange-Traded Commodity | Debt security backed by commodity/futures. | Medium |
XIV was an ETN, not an ETF. That single legal distinction is why XIV disappeared while SVXY survived.
3. The product, dissected: VelocityShares Daily Inverse VIX Short-Term ETN (XIV)
3.1 Legal structure — an unsecured debt obligation, not a fund
XIV was issued by Credit Suisse AG, Nassau Branch under a senior medium-term notes programme. It was marketed by VelocityShares (a Janus Capital subsidiary after 2014) but the issuer of record was Credit Suisse’s balance sheet.
| Attribute | XIV |
|---|---|
| Legal wrapper | Senior unsecured medium-term note |
| Issuer | Credit Suisse AG, Nassau Branch |
| Marketer | VelocityShares (Janus Capital) |
| Listed | NASDAQ, 29 Nov 2010 |
| CUSIP | 22542D506 |
| Stated maturity | 4 Dec 2030 (20 years) |
| Tracking index | SPVXSP — S&P 500 VIX Short-Term Futures Inverse Daily Index |
| Target exposure | −1× the daily return of SPVXSP |
| Expense / fee | 1.35% annual investor fee accrued daily against NAV |
| Peak AUM (late Jan 2018) | ~$1.9 bn |
| Holders got paid from | Credit Suisse’s general corporate assets |
An ETN investor does not own any VIX futures. They own an IOU from Credit Suisse that tracks a VIX-futures index. This is the biggest misconception retail traders had: it looked like a fund, but it was a bond.
3.2 The index — SPVXSP
XIV was pegged to the inverse daily return of the S&P 500 VIX Short-Term Futures Index (SPVXSP), a daily-rebalanced basket of front-month and second-month VIX futures weighted to maintain 30-day constant maturity. This rebalance is why short-vol positions earned roll yield in contango — and why they bled daily in backwardation.
3.3 Daily reset — the hedging mechanism
XIV targeted −1× daily return, a daily objective not cumulative. Every afternoon at 4:15 p.m. ET the notional was reset to ensure the next day’s return would again be −1× SPVXSP. When SPVXSP went up x% on the day, XIV’s NAV went down x%, and the hedging counterparty had to buy more VIX futures at the close — exactly the wrong direction at the wrong time.
3.4 The acceleration clause — the nuclear button
Page S-43 of the pricing supplement gave Credit Suisse the right to accelerate the notes if:
“The Intraday Indicative Value on any trading day is equal to or less than 20% of the prior day’s Closing Indicative Value.”
On 5 Feb 2018: Friday CIV $108.37 → Monday post-close $4.22 = −96%. Acceleration announced Tuesday 6:00 a.m. ET; last trade day 20 Feb; final redemption $5.99.

3.5 The issuer’s side — why Credit Suisse didn’t lose
Credit Suisse allegedly ran a proprietary hedge book, buying VIX futures ahead of the 4:15 p.m. rebalance it knew it would have to execute on behalf of XIV holders. Net gain: ~$475 mm; XIV holders’ claimed losses: ~$1.8 bn; Second Circuit revived market-manipulation claims April 2021; separate securities-fraud class settled $32.5 mm in 2023.

3.6 Family of twins — the short-vol ETP complex
| Product | Wrapper | Issuer | Leverage | AUM at peak | 5 Feb loss | Fate |
|---|---|---|---|---|---|---|
| XIV | ETN | Credit Suisse | −1.0× | ~$1.9 bn | −96% | Accelerated & terminated |
| SVXY | ETF | ProShares | −1.0× | ~$2.0 bn | −91% | De-levered to −0.5× on 28 Feb 2018 |
| VMIN | ETF | REX Shares | −1.0× actively managed | ~$0.5 bn | −87% | Survived; later repositioned |
Only XIV had the 80% acceleration clause and the bank-balance-sheet counterparty. That is why only XIV disappeared.
4. Cold open: 3:35 p.m. ET, 5 February 2018
At 3:35 p.m. Eastern a trader I follow tweeted that the VIX-futures basket looked “restrained” relative to the move in spot. Fifty minutes later SPVXSP was up 97% on the day, with 80% of that move in the last 50 minutes of regular trading. By the 4:15 p.m. rebalance print the inverse-vol ETP complex was dead.
5. The wider context — this happens every 3-4 years
| Year | Event | Forced-hedging product | Trigger |
|---|---|---|---|
| 1987 | Black Monday | Portfolio insurance | Falls → forced futures selling |
| 1998 | LTCM | Levered RV trades (~$1.25 trn) | Russia default → margin calls |
| 2007 | Quant Quake | Crowded stat-arb | Deleveraging cascade |
| 2008 | GFC / AIG | Rating-trigger collateral calls | Downgrades |
| 2015 | SNB peg break | FX brokers short CHF | Un-peg → stops cascade |
| 2018 | Volmageddon | Inverse-vol ETPs | VIX +116% → 4:15 forced buying |
| 2020 | COVID dash-for-cash | Risk-parity / levered ETFs | Vol spike |
| 2021 | Archegos | Total-return swaps | Prime-broker cascade |
| 2022 | UK LDI gilts | Pension LDI collateral | Mini-budget → BoE rescue |
| 2024 | Yen carry unwind | JPY carry + margin | BoJ hike |
The five-part machine — identical across all ten: (1) mechanical rule that forces direction, (2) crowded positioning, (3) size vs liquidity, (4) initial shock, (5) non-linear amplification.
6. Why it detonated — the reflexive loop
7. The algorithm — Example 8, Chapter 6
Features: vix (CBOE close), atr (22-day ATR), std (22-day rolling σ).
Label: weekly_low_return — % return from Monday open to the 5-session min low.
The L1 penalty zeroes weak factors and produces a coefficient path readable as a regime indicator. As VIX climbed in late January 2018, the predicted weekly low widens and the stop triggers earlier.
8. The backtest — three algorithms, one C# file
| Strategy | Class |
|---|---|
| Buy & Hold SVXY | VolmageddonBuyHoldAlgorithm |
| Fixed 5% stop | VolmageddonFixedStopAlgorithm |
| LASSO stop (Ex 8.2) | VolmageddonLassoStopAlgorithm |
Universe: SVXY + VIX via AddData<CBOE>("VIX", Resolution.Daily).
Window: 1 Jan 2016 → 28 Feb 2018. Starting cash: $100,000.
Engineering: hand-rolled coordinate-descent LASSO solver — no external dependency.
9. When and how to use each algorithm — a practitioner’s decision guide
This is the section the research brief implicitly points to but never names: which algorithm belongs in which part of your portfolio, and under what conditions? Here is my practitioner’s cut, grounded in the three backtests I actually ran on QuantConnect.
9.1 When to use Algorithm A — Buy & Hold (VolmageddonBuyHoldAlgorithm)
// =============================================================================
// Volmageddon Backtest — Strategy A: Buy & Hold SVXY (baseline)
// Book reference: Chapter 6, Example 8 (Stop Loss Based on Historical Volatility
// and Drawdown Recovery), Hands-On AI Trading with Python,
// QuantConnect and AWS (Pik, Chan, Broad, Sun, Singh — Wiley 2025)
//
// Purpose: the "no risk management" baseline — 100% long SVXY, held continuously
// through Volmageddon (5 Feb 2018). This is the equity curve the book's
// Example 8 stop-loss is designed to rescue.
//
// Window: 2016-01-01 -> 2018-02-28
// Capital: $100,000
// Symbol: SVXY (Raw prices so the real -91% Feb 5 2018 move is visible.)
//
// HOW TO RUN
// 1. Create a new C# Algorithm project in QuantConnect.
// 2. Replace Main.cs with this file.
// 3. Run the backtest. Export the tearsheet for the article.
// =============================================================================
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Securities;
namespace Volmageddon
{
public class VolmageddonBuyHoldAlgorithm : QCAlgorithm
{
private Symbol _svxy;
public override void Initialize()
{
SetStartDate(2016, 1, 1);
SetEndDate(2018, 2, 28);
SetCash(100000);
// Raw normalization — we want the real Feb-5-2018 collapse visible
// in the equity curve, not the split-adjusted version.
var svxy = AddEquity("SVXY", Resolution.Minute,
dataNormalizationMode: DataNormalizationMode.Raw);
svxy.SetLeverage(1m);
_svxy = svxy.Symbol;
SetBenchmark("SPY");
}
public override void OnData(Slice slice)
{
if (!Portfolio.Invested && Securities[_svxy].HasData)
{
SetHoldings(_svxy, 1.0);
}
}
}
}
Use it as: a benchmark, almost never as a live strategy.
Good for:
- Sanity-checking the alpha of any risk-managed variant. If your stop strategy does not beat buy-and-hold on a 5-year sample, your stop is just drag.
- Pedagogy — explaining roll-yield compounding in contango regimes to a non-quant audience.
- Regime research — overlay buy-and-hold equity curves from different regimes to see what contango looks like when it’s working.
Bad for:
- Any real capital on a product with a daily reset, an acceleration clause, or negative skew. The single-day cliff is not a tail risk; it is an inevitability over a long-enough horizon.
- ETN wrappers of any kind (credit risk is un-hedgeable without CDS).
Practical rule: allocate buy-and-hold only to instruments where the worst realistic 1-day move is ≤ the amount you are willing to lose. For SVXY that number is −90%. Almost nobody’s position-sizing model tolerates that.
9.2 When to use Algorithm B — Fixed 5% stop (VolmageddonFixedStopAlgorithm)
// =============================================================================
// Volmageddon Backtest — Strategy B: Fixed-Percentage Stop Loss
// Book reference: Chapter 6, Example 8.1 (Stop Loss Based on Historical
// Volatility and Drawdown Recovery), Hands-On AI Trading with
// Python, QuantConnect and AWS
// (Pik, Chan, Broad, Sun, Singh — Wiley 2025)
//
// Strategy: Every Monday just after market open, buy SVXY with a fixed 5%
// stop-loss order. Exit on Monday next week if the stop did not fire.
// This isolates the benefit of ANY stop-loss discipline vs. the LASSO variant.
//
// Window: 2016-01-01 -> 2018-02-28
// Capital: $100,000
// Symbol: SVXY (Raw prices so the real Feb 5 2018 move is visible.)
//
// HOW TO RUN
// 1. Create a new C# Algorithm project in QuantConnect.
// 2. Replace Main.cs with this file.
// 3. Run the backtest. Export the tearsheet for the article.
// =============================================================================
using System;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Orders;
using QuantConnect.Securities;
namespace Volmageddon
{
public class VolmageddonFixedStopAlgorithm : QCAlgorithm
{
private Symbol _svxy;
private OrderTicket _stopTicket;
private const decimal StopLossPercent = 0.95m; // 5% stop-loss from entry
public override void Initialize()
{
SetStartDate(2016, 1, 1);
SetEndDate(2018, 2, 28);
SetCash(100000);
var svxy = AddEquity("SVXY", Resolution.Minute,
dataNormalizationMode: DataNormalizationMode.Raw);
svxy.SetLeverage(1m);
_svxy = svxy.Symbol;
SetBenchmark("SPY");
// Weekly rhythm — enter Monday +2 min, liquidate next Monday -30 min.
Schedule.On(DateRules.WeekStart(_svxy),
TimeRules.AfterMarketOpen(_svxy, 2),
Enter);
Schedule.On(DateRules.WeekStart(_svxy),
TimeRules.AfterMarketOpen(_svxy, -30),
ExitPosition);
}
private void Enter()
{
if (!Securities[_svxy].HasData) return;
var qty = CalculateOrderQuantity(_svxy, 1m);
if (qty == 0) return;
MarketOrder(_svxy, qty);
var entryPrice = Securities[_svxy].Open > 0m
? Securities[_svxy].Open
: Securities[_svxy].Price;
var stopPrice = Math.Round(entryPrice * StopLossPercent, 2);
_stopTicket = StopMarketOrder(_svxy, -qty, stopPrice);
Log($"[{Time:yyyy-MM-dd HH:mm}] Enter {qty} SVXY @ {entryPrice:F2}, stop @ {stopPrice:F2}");
}
private void ExitPosition()
{
if (_stopTicket != null && _stopTicket.Status.IsOpen())
{
_stopTicket.Cancel();
_stopTicket = null;
}
if (Portfolio[_svxy].Invested)
{
Liquidate(_svxy);
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled) return;
if (_stopTicket != null && orderEvent.OrderId == _stopTicket.OrderId)
{
Log($"[{Time:yyyy-MM-dd HH:mm}] STOP HIT @ {orderEvent.FillPrice:F2}");
_stopTicket = null;
}
}
}
}
Use it as: a floor-level risk filter in low-turnover, symmetric, moderately-volatile instruments.
Good for:
- Single-name equities with steady realised vol (KO, consumer staples, utilities).
- A systematic “I will not let any single trade bleed more than 5%” discipline for discretionary traders.
- Weekly-rebalanced factor books where idiosyncratic vol dominates.
Bad for:
- Instruments with regime-shifting vol. A 5% stop in a VIX-10 world is a 0.1-sigma stop in a VIX-50 world — it triggers on noise and misses the tail.
- Gap-risk products. On 5 Feb 2018 SVXY gapped through every conceivable fixed stop because the price moved 80% in 50 minutes. A fixed stop does not protect you from liquidity-hole moves.
- Any product with an issuer acceleration clause — the stop will fire at the acceleration NAV, not the pre-acceleration NAV.
Practical rule: use a fixed stop only when the instrument’s realised 1-day vol over the last 22 sessions is in the bottom quintile of the entire sample history. Otherwise the stop width is mis-calibrated.
Parameter choices:
- Stop width ≈ 2 × daily σ (never a fixed %).
- Re-entry cooldown ≥ 3 sessions to avoid whip-saw.
- Liquidation time ≥ 15 min before close to avoid MOC slippage.
9.3 When to use Algorithm C — LASSO stop (Ex 8.2) (VolmageddonLassoStopAlgorithm)
// =============================================================================
// Volmageddon Backtest — Strategy C: LASSO-Driven Dynamic Stop Loss
// Book reference: Chapter 6, Example 8.2 (Stop Loss Based on Historical
// Volatility and Drawdown Recovery), Hands-On AI Trading with
// Python, QuantConnect and AWS
// (Pik, Chan, Broad, Sun, Singh — Wiley 2025)
//
// Strategy: Every Monday just after market open, buy SVXY. Place a stop $0.01
// below a LASSO-predicted weekly low. The LASSO is retrained every Monday on
// a rolling 3-year window of daily observations:
// Features : { VIX close, ATR(22) on SVXY, StdDev(22) on SVXY close }
// Label : weekly_low_return = min(low_{t+1..t+5}) / open_t - 1
// Solver : coordinate-descent LASSO with L1 soft-thresholding (alpha=1e-4).
// This is a direct C# port of the book's Example 8.2 Python implementation,
// with no external ML dependencies.
//
// Window: 2016-01-01 -> 2018-02-28
// Capital: $100,000
// Symbol: SVXY (Raw prices so the real Feb 5 2018 move is visible.)
//
// HOW TO RUN
// 1. Create a new C# Algorithm project in QuantConnect.
// 2. Replace Main.cs with this file.
// 3. Run the backtest. Export the tearsheet for the article.
// =============================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Securities;
namespace Volmageddon
{
public class VolmageddonLassoStopAlgorithm : QCAlgorithm
{
// --------------------------------------------------------------------
// Config
// --------------------------------------------------------------------
private const int WindowDays = 22;
private const double LassoAlpha = 1e-4; // book's alpha_exponent=4
private const int TrainingDays = 3 * 252; // ~3 years
private const decimal StopBuffer = 0.01m; // $0.01 below predicted low
private const decimal FixedStopFallback = 0.95m; // used until enough data
// --------------------------------------------------------------------
// State
// --------------------------------------------------------------------
private Symbol _svxy;
private Symbol _vix;
private AverageTrueRange _atr;
private StandardDeviation _std;
private readonly List<DailyObservation> _daily = new List<DailyObservation>();
private OrderTicket _stopTicket;
private decimal _lastVixClose;
private bool _vixPrimed;
// --------------------------------------------------------------------
// Initialize
// --------------------------------------------------------------------
public override void Initialize()
{
SetStartDate(2016, 1, 1);
SetEndDate(2018, 2, 28);
SetCash(100000);
var svxy = AddEquity("SVXY", Resolution.Minute,
dataNormalizationMode: DataNormalizationMode.Raw);
svxy.SetLeverage(1m);
_svxy = svxy.Symbol;
// Cash index VIX (daily resolution is the only supported frequency).
_vix = AddIndex("VIX", Resolution.Daily).Symbol;
_atr = ATR(_svxy, WindowDays, MovingAverageType.Simple, Resolution.Daily);
_std = STD(_svxy, WindowDays, Resolution.Daily);
// Warm up: 3 years of history + indicator warmup.
SetWarmUp(TimeSpan.FromDays(365 * 3 + 45));
SetBenchmark("SPY");
// Weekly rhythm — enter Monday +2 min, liquidate next Monday -30 min.
Schedule.On(DateRules.WeekStart(_svxy),
TimeRules.AfterMarketOpen(_svxy, 2),
Enter);
Schedule.On(DateRules.WeekStart(_svxy),
TimeRules.AfterMarketOpen(_svxy, -30),
ExitPosition);
// Snapshot a daily row 1 minute before close for training data.
Schedule.On(DateRules.EveryDay(_svxy),
TimeRules.BeforeMarketClose(_svxy, 1),
SnapshotDay);
}
// --------------------------------------------------------------------
// Data
// --------------------------------------------------------------------
public override void OnData(Slice slice)
{
if (slice.Bars.TryGetValue(_vix, out var vixBar))
{
_lastVixClose = vixBar.Close;
_vixPrimed = true;
}
}
private void SnapshotDay()
{
if (IsWarmingUp) return;
if (!_atr.IsReady || !_std.IsReady || !_vixPrimed) return;
var bar = Securities[_svxy];
_daily.Add(new DailyObservation
{
Date = Time.Date,
Open = bar.Open,
High = bar.High,
Low = bar.Low,
Close = bar.Close,
Vix = _lastVixClose,
Atr = _atr.Current.Value,
Std = _std.Current.Value
});
if (_daily.Count > TrainingDays + 60)
_daily.RemoveAt(0);
}
// --------------------------------------------------------------------
// Enter / exit
// --------------------------------------------------------------------
private void Enter()
{
if (IsWarmingUp) return;
if (!Securities[_svxy].HasData) return;
var qty = CalculateOrderQuantity(_svxy, 1m);
if (qty == 0) return;
MarketOrder(_svxy, qty);
var entryPrice = Securities[_svxy].Open > 0m
? Securities[_svxy].Open
: Securities[_svxy].Price;
var stopPrice = ComputeLassoStop(entryPrice);
if (stopPrice <= 0m) return;
_stopTicket = StopMarketOrder(_svxy, -qty, stopPrice);
Log($"[{Time:yyyy-MM-dd HH:mm}] Enter {qty} SVXY @ {entryPrice:F2}, LASSO stop @ {stopPrice:F2}");
}
private void ExitPosition()
{
if (_stopTicket != null && _stopTicket.Status.IsOpen())
{
_stopTicket.Cancel();
_stopTicket = null;
}
if (Portfolio[_svxy].Invested)
{
Liquidate(_svxy);
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled) return;
if (_stopTicket != null && orderEvent.OrderId == _stopTicket.OrderId)
{
Log($"[{Time:yyyy-MM-dd HH:mm}] STOP HIT @ {orderEvent.FillPrice:F2}");
_stopTicket = null;
}
}
// --------------------------------------------------------------------
// LASSO fit + prediction
// --------------------------------------------------------------------
private decimal ComputeLassoStop(decimal entryPrice)
{
if (_daily.Count < 60)
return Math.Round(entryPrice * FixedStopFallback, 2);
var samples = BuildTrainingSamples();
if (samples.Count < 30)
return Math.Round(entryPrice * FixedStopFallback, 2);
var lasso = FitLasso(samples, LassoAlpha);
var features = new double[]
{
(double)_lastVixClose,
(double)_atr.Current.Value,
(double)_std.Current.Value
};
var predictedReturn = lasso.Predict(features);
var predictedLow = entryPrice * (1m + (decimal)predictedReturn);
var stop = Math.Round(predictedLow - StopBuffer, 2);
// Guards — never put stop at or above entry, or below zero.
if (stop >= entryPrice) stop = Math.Round(entryPrice * 0.99m, 2);
if (stop <= 0m) stop = Math.Round(entryPrice * 0.5m, 2);
Plot("StopLoss", "PredictedReturn", (decimal)predictedReturn);
Plot("StopLoss", "StopPrice", stop);
Plot("StopLoss", "EntryPrice", entryPrice);
return stop;
}
private List<(double[] X, double y)> BuildTrainingSamples()
{
var samples = new List<(double[] X, double y)>();
int n = _daily.Count;
for (int i = 0; i < n - 5; i++)
{
var d = _daily[i];
decimal minLow = decimal.MaxValue;
for (int k = i + 1; k <= i + 5; k++)
if (_daily[k].Low < minLow) minLow = _daily[k].Low;
if (d.Open <= 0m) continue;
double label = (double)((minLow / d.Open) - 1m);
samples.Add((new[] { (double)d.Vix, (double)d.Atr, (double)d.Std }, label));
}
if (samples.Count > TrainingDays)
samples = samples.Skip(samples.Count - TrainingDays).ToList();
return samples;
}
// --------------------------------------------------------------------
// Minimal dependency-free LASSO (coordinate descent + soft threshold)
// --------------------------------------------------------------------
private static LassoModel FitLasso(
List<(double[] X, double y)> samples,
double alpha,
int maxIter = 1000,
double tol = 1e-6)
{
int n = samples.Count;
int p = samples[0].X.Length;
var X = new double[n, p];
var y = new double[n];
for (int i = 0; i < n; i++)
{
for (int j = 0; j < p; j++) X[i, j] = samples[i].X[j];
y[i] = samples[i].y;
}
// Standardise features.
var mean = new double[p];
var std = new double[p];
for (int j = 0; j < p; j++)
{
double s = 0; for (int i = 0; i < n; i++) s += X[i, j];
mean[j] = s / n;
double ss = 0; for (int i = 0; i < n; i++) { var d = X[i, j] - mean[j]; ss += d * d; }
std[j] = Math.Sqrt(ss / n);
if (std[j] < 1e-12) std[j] = 1.0;
for (int i = 0; i < n; i++) X[i, j] = (X[i, j] - mean[j]) / std[j];
}
double yMean = 0; for (int i = 0; i < n; i++) yMean += y[i]; yMean /= n;
for (int i = 0; i < n; i++) y[i] -= yMean;
var beta = new double[p];
var colSqNorm = new double[p];
for (int j = 0; j < p; j++)
{
double s = 0; for (int i = 0; i < n; i++) s += X[i, j] * X[i, j];
colSqNorm[j] = s;
}
var resid = new double[n];
for (int i = 0; i < n; i++) resid[i] = y[i];
for (int it = 0; it < maxIter; it++)
{
double maxChange = 0;
for (int j = 0; j < p; j++)
{
double rho = 0;
for (int i = 0; i < n; i++) rho += X[i, j] * (resid[i] + X[i, j] * beta[j]);
double newBeta;
double thresh = alpha * n;
if (rho > thresh) newBeta = (rho - thresh) / colSqNorm[j];
else if (rho < -thresh) newBeta = (rho + thresh) / colSqNorm[j];
else newBeta = 0;
double delta = newBeta - beta[j];
if (Math.Abs(delta) > 0)
{
for (int i = 0; i < n; i++) resid[i] -= X[i, j] * delta;
beta[j] = newBeta;
maxChange = Math.Max(maxChange, Math.Abs(delta));
}
}
if (maxChange < tol) break;
}
return new LassoModel
{
Beta = beta,
FeatureMean = mean,
FeatureStd = std,
YMean = yMean
};
}
// --------------------------------------------------------------------
// Helper types
// --------------------------------------------------------------------
private class LassoModel
{
public double[] Beta { get; set; }
public double[] FeatureMean { get; set; }
public double[] FeatureStd { get; set; }
public double YMean { get; set; }
public double Predict(double[] rawFeatures)
{
double acc = YMean;
for (int j = 0; j < Beta.Length; j++)
{
double xs = (rawFeatures[j] - FeatureMean[j]) / FeatureStd[j];
acc += xs * Beta[j];
}
return acc;
}
}
private class DailyObservation
{
public DateTime Date { get; set; }
public decimal Open { get; set; }
public decimal High { get; set; }
public decimal Low { get; set; }
public decimal Close { get; set; }
public decimal Vix { get; set; }
public decimal Atr { get; set; }
public decimal Std { get; set; }
}
}
}
Use it as: a regime-aware downside shield for any instrument whose weekly low is a predictable function of cross-asset vol features.
Good for:
- Short-vol strategies (SVXY, SVIX, any future inverse-vol ETP).
- Long-equity strategies exposed to crowded short-vol positioning (most beta-1 equity).
- Carry trades — JPY, EM FX, credit — where realised vol compresses during the gain phase and explodes during the unwind.
- Any strategy whose P&L distribution is heavily left-skewed.
Bad for:
- Long-vol strategies. As I noted in the inversion section, UVXY’s weekly low is rarely a binding constraint in a rising-vol regime — the LASSO stop will never fire, and you may mistake that for safety.
- Strategies where the exit decision should be based on signal decay rather than price (statistical arbitrage, event-driven merger arb). A stop-loss is the wrong risk tool for those.
- Ultra-short holding periods (<1 day). The model is weekly-trained; intraday you need a different feature set (e.g. dealer gamma, 0-DTE skew).
Practical rule: use LASSO stops whenever (a) the asset has meaningful cross-sectional vol features available (VIX, ATR, σ), (b) the holding period is days-to-weeks, and (c) the P&L is asymmetric on the downside.
Parameter choices I use in production:
- Training window: 3 years rolling, retrained weekly. Shorter (1 yr) over-fits the current regime; longer (5 yr) is too slow to react.
- Features: VIX level, VIX 5-day change, ATR(22), σ(22), optionally VIX-futures term structure (front/second spread) and realised-implied spread.
- L1 penalty α: cross-validated weekly; typical range 0.01–0.10. Read the coefficient path — if
vixdominates, you are regime-dependent; ifatrdominates, you are idiosyncratic. - Stop placement: predicted weekly low − $0.01, placed as a GTC stop order Monday open.
- Fail-safe: if the model’s predicted low is > 20% below entry, don’t take the trade — the regime is too unstable to stop into.
9.4 When to graduate to Algorithm 8.3 — LASSO + protective put
Not implemented in this backtest, but worth flagging as the next step. Replace the stop with a long put whose strike ≈ the LASSO-predicted weekly low. Use this when:
- The instrument has a liquid listed options chain (SPY, QQQ, major single names).
- The stop-through risk (overnight gap, accelerated ETN) is non-trivial.
- You can pay the theta cost — which in low-vol regimes is cheap and in high-vol regimes is the point.
9.5 The master decision table
| Condition | Algorithm to use | Why |
|---|---|---|
| Benchmark a risk-managed variant | A — Buy & Hold | Zero-alpha reference line |
| Low-vol single-stock, symmetric P&L | B — Fixed stop | Simple, cheap, adequate |
| Short-vol ETP / carry trade / asymmetric downside | C — LASSO stop | Regime-adaptive width |
| Gap-risk product with listed options | 8.3 — LASSO + put | Hedges discontinuity |
| Long-vol product (UVXY, VXX) | None of these | Use trailing profit-taking instead |
| Intraday holding period | None — features wrong | Rebuild with dealer-gamma features |
| ETN wrapper with acceleration clause | Do not hold overnight | Acceleration bypasses all stops |
9.6 How I stack them in production
I run LASSO stops on every short-vol and carry sleeve, fixed 2×σ stops on the equity factor book, and zero stops on the long-vol hedges (trailing profit-takers instead). Buy-and-hold only runs as a paper benchmark in my QuantConnect research environment. This is the architecture that survives a Volmageddon without blowing up on a quiet Tuesday.
10. The hero chart

- Buy-and-Hold compounds nicely, then −91% in a session.
- Fixed 5% stop caps the single-week loss but chops on re-entries.
- LASSO stop flattens to cash in the final week as the predicted low widens.
11. The litigation and the lesson
Credit Suisse allegedly netted ~$475 mm hedging gains vs $1.8 bn investor losses; Second Circuit revived market-manipulation claims; separate securities-fraud class settled for $32.5 mm in 2023. SVXY de-levered to −0.5×; XIV was liquidated. A more conservative SVIX has since re-entered the market.
Lesson: (1) know the wrapper — an ETN can disappear overnight; (2) if your stop is not a function of the regime, it is a wish; (3) assume a reflexive blow-up every 3–4 years and build your framework around it.
12. Alternative perspectives
- Inversion — what would have killed this model? A long-vol UVXY position. Example 8 is asymmetric downside protection.
- Second-order effect — Volmageddon 2.0 priced in 0-DTE gamma.
- Cross-domain analogy — NTSB crash reports. Cause, sequence, contributing factors, corrective action.
- The uncomfortable one — central banks bail out levered institutions, not retail ETPs. That asymmetry is priced into every crowded trade.
13. What the book teaches that this post doesn’t
Example 8 is one of twenty fully coded examples in Chapter 6 alone:
- Ex 11 — Inverse Volatility Rank (Ch 6)
- Ex 12 — Trading Costs Optimization (Ch 6)
- Ch 7 — RL Hedging
- Ch 8 — Conditional Portfolio Optimization through regime changes
- Ch 9 — LLM-based news triggers
HandsOnAITradingBook GitHub repo → Hands-On AI Trading with Python, QuantConnect and AWS.
14. Two provoking questions
- Given reflexive blow-ups arrive every 3–4 years, what is the most probable vector for the next one — 0-DTE SPX gamma, private-credit NAV lending, or stablecoin redemption stress?
- If every major reflexive event since 1998 has been absorbed by a central bank or prime broker, what are you pricing into your Sharpe when you assume the next one will be too?
Disclaimer: historical backtest on SVXY, not investment advice. Past performance does not guarantee future results. Do not trade inverse-volatility ETPs without understanding reset mechanics, wrapper type, and acceleration clauses.
