Volmageddon & The 90-Second Rule: The $0.01 Stop That Would Have Saved $3 Billion

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.

A person in a hoodie sits at a desk with their head in their hands, surrounded by monitors showing a crashing stock chart labeled VIX and warning signs such as CRASH and VOLMAGEDDON. Empty cups and papers are scattered on the desk.

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 examplesHands-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:

WrapperStands forWhat it isCredit risk?
ETFExchange-Traded FundHolds a basket of underlying assets.Low — ring-fenced
ETNExchange-Traded NoteAn unsecured debt promise from an issuing bank.High — bank can “accelerate”
ETCExchange-Traded CommodityDebt 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)

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.

AttributeXIV
Legal wrapperSenior unsecured medium-term note
IssuerCredit Suisse AG, Nassau Branch
MarketerVelocityShares (Janus Capital)
ListedNASDAQ, 29 Nov 2010
CUSIP22542D506
Stated maturity4 Dec 2030 (20 years)
Tracking indexSPVXSP — S&P 500 VIX Short-Term Futures Inverse Daily Index
Target exposure−1× the daily return of SPVXSP
Expense / fee1.35% annual investor fee accrued daily against NAV
Peak AUM (late Jan 2018)~$1.9 bn
Holders got paid fromCredit 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.

Line chart showing VIX close values from Jan 26 to Feb 9, 2018, highlighting a sharp spike of over 110% on Feb 5, known as Volmageddon, before values decrease but remain elevated afterward.

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.

Bar chart showing one-day drawdown percentages for three products on Feb 5, 2018: XIV (-96%), SVXY (-91%), and VMIN (-87%); SPX index fell -4.1%. Title and source are also shown.

3.6 Family of twins — the short-vol ETP complex

ProductWrapperIssuerLeverageAUM at peak5 Feb lossFate
XIVETNCredit Suisse−1.0×~$1.9 bn−96%Accelerated & terminated
SVXYETFProShares−1.0×~$2.0 bn−91%De-levered to −0.5× on 28 Feb 2018
VMINETFREX 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

YearEventForced-hedging productTrigger
1987Black MondayPortfolio insuranceFalls → forced futures selling
1998LTCMLevered RV trades (~$1.25 trn)Russia default → margin calls
2007Quant QuakeCrowded stat-arbDeleveraging cascade
2008GFC / AIGRating-trigger collateral callsDowngrades
2015SNB peg breakFX brokers short CHFUn-peg → stops cascade
2018VolmageddonInverse-vol ETPsVIX +116% → 4:15 forced buying
2020COVID dash-for-cashRisk-parity / levered ETFsVol spike
2021ArchegosTotal-return swapsPrime-broker cascade
2022UK LDI giltsPension LDI collateralMini-budget → BoE rescue
2024Yen carry unwindJPY carry + marginBoJ 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

#CauseWhy it mattered
1Flawed product architectureDaily resets always reinforce direction 
2Overreliance on 2011-2017 historyNear-zero probability on a 100% futures move 
3$5 bn AUM vs $7 bn futures notionalETP resets were the market at 4:15 
4VIX spike unmatched by SPXDealer gamma amplified the VIX calculation 

7. The algorithm — Example 8, Chapter 6

VariantStop ruleModel
8.1Fixed-% stop (e.g. 95% of entry)None 
8.2Stop $0.01 below LASSO-predicted weekly lowLASSO on {VIX, ATR(22), σ(22)} → weekly-low return 
8.3Protective put at predicted lowLASSO + options chain 

Featuresvix (CBOE close), atr (22-day ATR), std (22-day rolling σ).
Labelweekly_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

StrategyClass
Buy & Hold SVXYVolmageddonBuyHoldAlgorithm 
Fixed 5% stopVolmageddonFixedStopAlgorithm 
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 vix dominates, you are regime-dependent; if atr dominates, 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

ConditionAlgorithm to useWhy
Benchmark a risk-managed variantA — Buy & HoldZero-alpha reference line
Low-vol single-stock, symmetric P&LB — Fixed stopSimple, cheap, adequate
Short-vol ETP / carry trade / asymmetric downsideC — LASSO stopRegime-adaptive width
Gap-risk product with listed options8.3 — LASSO + putHedges discontinuity
Long-vol product (UVXY, VXX)None of theseUse trailing profit-taking instead
Intraday holding periodNone — features wrongRebuild with dealer-gamma features
ETN wrapper with acceleration clauseDo not hold overnightAcceleration 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

Line graph showing equity growth from 2016 to Feb 2018 for three SVXY trading strategies: Buy & Hold, Fixed 5% Stop, and LASSO Stop. Buy & Hold drops sharply near Feb 2018, while others perform better.
  • 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

  1. Inversion — what would have killed this model? A long-vol UVXY position. Example 8 is asymmetric downside protection.
  2. Second-order effect — Volmageddon 2.0 priced in 0-DTE gamma.
  3. Cross-domain analogy — NTSB crash reports. Cause, sequence, contributing factors, corrective action.
  4. 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

  1. 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?
  2. 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.

keyboard_arrow_up
Index