Selective Long VIXY Hedge Strategy
A hedged equity strategy applied to ten large-cap NASDAQ stocks. The strategy holds an equal-weight long position across the stock universe at all times, and selectively adds a long VIXY position during volatility spikes, when the 5-day moving average of VIXY closes at or above its 60-day upper Bollinger Band (mean + 1.5 standard deviations). The hedge is sized at 50% of the portfolio and is intended to offset equity drawdowns during high-volatility regimes.
Results are compared against two baselines: an equal-weight buy-and-hold portfolio across the same ten stocks, and QQQ as a passive NASDAQ market proxy.
Data source and setup
This notebook uses Tiingo daily adjusted price data for all stocks, QQQ, and VIXY.
To run the notebook, you must provide a Tiingo API key through a local .env file. The code expects the variable below to be available in the environment:
import os
import time
import requests
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as mticker
from pathlib import Path
from dotenv import load_dotenv
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from IPython.display import display, HTML
FL_BLUE = '#2563eb'
FL_SLATE = '#64748b'
FL_AMBER = '#f59e0b'
FL_RED = '#ef4444'
FL_BG = '#ffffff'
FL_GRID = '#e2e8f0'
FL_TEXT = '#0f172a'
FL_TEXT2 = '#334155'
FL_BORDER = '#e2e8f0'
STRATEGY_NAME = 'Selective Long VIXY Hedge'
COLORS = {
STRATEGY_NAME: FL_BLUE,
'Equal Weighted Buy and Hold': FL_SLATE,
'QQQ Buy and Hold': FL_AMBER,
}
LWIDTHS = {
STRATEGY_NAME: 2.0,
'Equal Weighted Buy and Hold': 1.5,
'QQQ Buy and Hold': 1.5,
}
matplotlib.rcParams.update({
'figure.facecolor': FL_BG,
'axes.facecolor': FL_BG,
'axes.edgecolor': FL_BORDER,
'axes.labelcolor': FL_TEXT2,
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.color': FL_GRID,
'grid.linewidth': 0.7,
'xtick.color': FL_TEXT2,
'ytick.color': FL_TEXT2,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'axes.labelsize': 11,
'axes.titlesize': 12,
'axes.titlecolor': FL_TEXT,
'axes.titlepad': 12,
'legend.frameon': False,
'legend.fontsize': 10,
'figure.dpi': 300,
'savefig.bbox': 'tight',
'font.family': 'sans-serif',
'font.sans-serif': ['Inter', 'Helvetica Neue', 'Arial', 'DejaVu Sans'],
})
PROJECT_ROOT = Path.cwd()
load_dotenv(PROJECT_ROOT / '.env')
TIINGO_API_KEY = os.getenv('TIINGO_API_KEY')
if not TIINGO_API_KEY:
raise RuntimeError('TIINGO_API_KEY is missing from the project-level .env')
FIXED_TICKERS = ['AAPL','MSFT','NVDA','GOOGL','META','AMZN','AVGO','TSLA','NFLX','COST']
STARTING_CAPITAL = 100_000.0
START_DATE = '2018-01-02'
REQUEST_SLEEP = 0.15
INCLUDE_QQQ_BASELINE = True
# VIXY hedge parameters
VIXY_SIGNAL_MA = 5 # short MA on VIXY close
VIXY_BAND_WINDOW = 60 # rolling window for mean + std
VIXY_BAND_STD = 1.5 # upper band multiplier
HEDGE_RATIO = 0.5 # VIXY allocation relative to stock portfolio
def make_session():
session = requests.Session()
session.mount('https://', HTTPAdapter(max_retries=Retry(
total=5, backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=['GET']
)))
return session
def fetch_tiingo_prices(session, ticker, start_date):
resp = session.get(
f'https://api.tiingo.com/tiingo/daily/{ticker}/prices',
headers={'Authorization': f'Token {TIINGO_API_KEY}', 'Content-Type': 'application/json'},
params={'startDate': start_date, 'resampleFreq': 'daily', 'format': 'json'},
timeout=60
)
resp.raise_for_status()
raw = pd.DataFrame(resp.json())
if raw.empty:
raise ValueError(f'No price data returned for {ticker}')
df = pd.DataFrame({
'date': pd.to_datetime(raw['date'], utc=True).dt.tz_localize(None),
'open': pd.to_numeric(raw['adjOpen'], errors='coerce'),
'high': pd.to_numeric(raw['adjHigh'], errors='coerce'),
'low': pd.to_numeric(raw['adjLow'], errors='coerce'),
'close': pd.to_numeric(raw['adjClose'], errors='coerce'),
'volume': pd.to_numeric(raw['adjVolume'], errors='coerce'),
})
df = df.dropna(subset=['date','open','high','low','close','volume']).sort_values('date').reset_index(drop=True)
if df.empty:
raise ValueError(f'Processed dataframe is empty for {ticker}')
return df
def build_daily_equity_curve_from_returns(daily_returns_df, starting_capital):
if daily_returns_df.empty:
return pd.DataFrame(columns=['date','daily_return','equity','cum_pnl','daily_pnl'])
eq = daily_returns_df.copy().sort_values('date').reset_index(drop=True)
eq['equity'] = starting_capital * (1 + eq['daily_return']).cumprod()
eq['cum_pnl'] = eq['equity'] - starting_capital
eq['daily_pnl'] = eq['equity'].diff().fillna(eq['equity'] - starting_capital)
return eq
def calc_cagr_and_sharpe(daily_equity_df, starting_capital):
if daily_equity_df.empty:
return 0.0, 0.0
years = (daily_equity_df['date'].max() - daily_equity_df['date'].min()).days / 365.25
ending = daily_equity_df['equity'].iloc[-1]
cagr = (ending / starting_capital) ** (1 / years) - 1 if years > 0 and ending > 0 else 0.0
dr = daily_equity_df['daily_return'].dropna()
sharpe = (dr.mean() / dr.std(ddof=1)) * (252 ** 0.5) if len(dr) > 1 and dr.std(ddof=1) > 0 else 0.0
return round(cagr, 6), round(sharpe, 6)
def build_summary_row(strategy_name, equity_df):
if not equity_df.empty:
total_pnl = equity_df['cum_pnl'].iloc[-1]
ending = equity_df['equity'].iloc[-1]
max_dd = (equity_df['equity'] - equity_df['equity'].cummax()).min()
cagr, sharpe = calc_cagr_and_sharpe(equity_df[['date','daily_return','equity']].copy(), STARTING_CAPITAL)
start_dt, end_dt = equity_df['date'].min(), equity_df['date'].max()
else:
total_pnl = ending = max_dd = cagr = sharpe = 0.0
start_dt = end_dt = pd.NaT
return {
'Strategy': strategy_name,
'Start': start_dt.strftime('%Y-%m-%d') if pd.notna(start_dt) else '',
'End': end_dt.strftime('%Y-%m-%d') if pd.notna(end_dt) else '',
'Ending Equity': round(float(ending), 2),
'Total PnL': round(float(total_pnl), 2),
'Max Drawdown': round(float(max_dd), 2),
'CAGR': cagr,
'Sharpe': sharpe,
}
def fmt_dollar(v): return f'${v:>12,.0f}'
def fmt_pct(v): return f'{v:.2%}'
def fmt_f2(v): return f'{v:.2f}'
selected_tickers = FIXED_TICKERS.copy()
session = make_session()
price_cache = {}
fetch_rows = []
for i, ticker in enumerate(selected_tickers, start=1):
try:
time.sleep(REQUEST_SLEEP)
df_prices = fetch_tiingo_prices(session, ticker, START_DATE)
price_cache[ticker] = df_prices.set_index('date').copy()
fetch_rows.append({'Ticker': ticker, 'Status': 'OK',
'First': df_prices['date'].min().strftime('%Y-%m-%d'),
'Last': df_prices['date'].max().strftime('%Y-%m-%d'),
'Rows': len(df_prices), 'Error': ''})
print(f'{i:2}/{len(selected_tickers)} {ticker:<6} OK {len(df_prices)} rows')
except Exception as e:
fetch_rows.append({'Ticker': ticker, 'Status': 'ERROR',
'First': '', 'Last': '', 'Rows': None, 'Error': str(e)})
print(f'{i:2}/{len(selected_tickers)} {ticker:<6} ERROR {e}')
# Fetch VIXY
time.sleep(REQUEST_SLEEP)
vixy_price_df = fetch_tiingo_prices(session, 'VIXY', START_DATE)
print(f' VIXY OK {len(vixy_price_df)} rows')
if INCLUDE_QQQ_BASELINE:
time.sleep(REQUEST_SLEEP)
qqq_price_df = fetch_tiingo_prices(session, 'QQQ', START_DATE)
print(f' QQQ OK {len(qqq_price_df)} rows')
else:
qqq_price_df = pd.DataFrame(columns=['date','open','high','low','close','volume'])
fetch_summary_df = pd.DataFrame(fetch_rows).sort_values(['Status','Ticker']).reset_index(drop=True)
1/10 AAPL OK 2109 rows
2/10 MSFT OK 2109 rows
3/10 NVDA OK 2109 rows
4/10 GOOGL OK 2109 rows
5/10 META OK 2109 rows
6/10 AMZN OK 2109 rows
7/10 AVGO OK 2109 rows
8/10 TSLA OK 2109 rows
9/10 NFLX OK 2109 rows
10/10 COST OK 2109 rows
VIXY OK 2109 rows
QQQ OK 2109 rows
def run_vixy_hedge_strategy(tickers, price_cache, vixy_price_df, hedge_ratio):
# Stock portfolio: equal-weight daily returns
stock_frames = []
for ticker in tickers:
if ticker not in price_cache:
continue
df = price_cache[ticker][['close']].copy()
df['return'] = df['close'].pct_change()
stock_frames.append(df[['return']].rename(columns={'return': ticker}))
if not stock_frames:
return {
'equity_df': pd.DataFrame(columns=['date','daily_return','equity','cum_pnl','daily_pnl']),
'signal_series': pd.Series(dtype=float),
'vixy_df': pd.DataFrame(),
'tickers_used': 0,
}
stock_returns = pd.concat(stock_frames, axis=1).sort_index().mean(axis=1, skipna=True)
# VIXY signal
v = vixy_price_df[['date','close']].copy().sort_values('date').reset_index(drop=True)
v['vixy_ma'] = v['close'].rolling(VIXY_SIGNAL_MA).mean()
v['vixy_band_mean'] = v['close'].rolling(VIXY_BAND_WINDOW).mean()
v['vixy_band_std'] = v['close'].rolling(VIXY_BAND_WINDOW).std()
v['vixy_upper'] = v['vixy_band_mean'] + VIXY_BAND_STD * v['vixy_band_std']
v['signal'] = np.where(v['vixy_ma'] >= v['vixy_upper'], 1, 0)
v['vixy_return'] = v['close'].pct_change()
# Hedge return: signal today --> VIXY return next day (forward-shifted)
v['hedge_return'] = v['signal'] * v['vixy_return'].shift(-1) * hedge_ratio
v = v.set_index('date')
signal_series = v['signal']
combined = pd.concat(
[stock_returns.rename('stock'), v['hedge_return'].rename('hedge')],
axis=1
).sort_index()
combined['stock'] = combined['stock'].fillna(0.0)
combined['hedge'] = combined['hedge'].fillna(0.0)
combined['daily_return'] = (combined['stock'] + combined['hedge']) / (1 + hedge_ratio)
dr_df = (
combined[['daily_return']]
.dropna()
.reset_index()
.rename(columns={'index': 'date', 'date': 'date'})
)
# handle both index name cases
if 'date' not in dr_df.columns:
dr_df = dr_df.reset_index().rename(columns={'index': 'date'})
equity_df = build_daily_equity_curve_from_returns(dr_df, STARTING_CAPITAL)
return {
'equity_df': equity_df,
'signal_series': signal_series,
'vixy_df': v,
'tickers_used': len(stock_frames),
}
def run_equal_weight_buy_and_hold(tickers, price_cache):
frames = []
for ticker in tickers:
if ticker not in price_cache:
continue
df = price_cache[ticker].copy()
if df.empty:
continue
df['asset_return'] = df['close'].pct_change()
frames.append(df[['asset_return']].rename(columns={'asset_return': ticker}))
if not frames:
return {'equity_df': pd.DataFrame(columns=['date','daily_return','equity','cum_pnl','daily_pnl']), 'tickers_used': 0}
ret_df = pd.concat(frames, axis=1).sort_index()
dr_df = ret_df.mean(axis=1, skipna=True).rename('daily_return').dropna().reset_index().rename(columns={'index': 'date'})
return {'equity_df': build_daily_equity_curve_from_returns(dr_df, STARTING_CAPITAL), 'tickers_used': len(ret_df.columns)}
def run_qqq_buy_and_hold(qqq_price_df):
if qqq_price_df.empty:
return {'equity_df': pd.DataFrame(columns=['date','daily_return','equity','cum_pnl','daily_pnl']), 'tickers_used': 0}
w = qqq_price_df[['date','close']].copy()
w['daily_return'] = w['close'].pct_change()
dr_df = w[['date','daily_return']].dropna().reset_index(drop=True)
return {'equity_df': build_daily_equity_curve_from_returns(dr_df, STARTING_CAPITAL), 'tickers_used': 1}
vixy_result = run_vixy_hedge_strategy(selected_tickers, price_cache, vixy_price_df, HEDGE_RATIO)
ew_bh_result = run_equal_weight_buy_and_hold(selected_tickers, price_cache)
qqq_bh_result = run_qqq_buy_and_hold(qqq_price_df)
vixy_equity_df = vixy_result['equity_df']
ew_bh_equity_df = ew_bh_result['equity_df']
qqq_bh_equity_df = qqq_bh_result['equity_df']
vixy_signal = vixy_result['signal_series']
vixy_df = vixy_result['vixy_df']
comparison_frames, drawdown_frames = [], []
strategy_specs = [
(STRATEGY_NAME, vixy_equity_df, 'result'),
('Equal Weighted Buy and Hold', ew_bh_equity_df, 'baseline'),
('QQQ Buy and Hold', qqq_bh_equity_df, 'baseline'),
]
for name, eq_df, role in strategy_specs:
if eq_df.empty:
continue
row = eq_df[['date','daily_return','daily_pnl','cum_pnl','equity']].copy()
row['Strategy'] = name
comparison_frames.append(row)
dd = eq_df[['date','equity']].copy()
dd['drawdown'] = dd['equity'] - dd['equity'].cummax()
dd['Strategy'] = name
drawdown_frames.append(dd)
comparison_equity_df = pd.concat(comparison_frames, ignore_index=True) if comparison_frames else pd.DataFrame()
drawdown_comparison_df = pd.concat(drawdown_frames, ignore_index=True) if drawdown_frames else pd.DataFrame()
summary_rows = [
build_summary_row(STRATEGY_NAME, vixy_equity_df),
build_summary_row('Equal Weighted Buy and Hold', ew_bh_equity_df),
build_summary_row('QQQ Buy and Hold', qqq_bh_equity_df),
]
summary_df = pd.DataFrame(summary_rows)
Universe
The stock portfolio uses a fixed list of ten large-cap NASDAQ stocks. The hedge instrument is VIXY (the ProShares VIX Short-Term Futures ETF) which tracks front-month VIX futures and rises sharply during equity market stress.
selected_tickers
['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'META', 'AMZN', 'AVGO', 'TSLA', 'NFLX', 'COST']
Data coverage
Daily adjusted OHLCV prices are fetched from Tiingo starting January 2018 for all ten stock tickers. VIXY prices are fetched separately over the same period.
summary_view = fetch_summary_df[['Ticker', 'Status', 'First', 'Last', 'Rows']].reset_index(drop=True)
display(summary_view)
| Ticker | Status | First | Last | Rows | |
|---|---|---|---|---|---|
| 0 | AAPL | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 1 | AMZN | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 2 | AVGO | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 3 | COST | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 4 | GOOGL | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 5 | META | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 6 | MSFT | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 7 | NFLX | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 8 | NVDA | OK | 2018-01-02 | 2026-05-22 | 2109 |
| 9 | TSLA | OK | 2018-01-02 | 2026-05-22 | 2109 |
Strategy
Stock leg. An equal-weight long position is held across all ten stocks every day. Unlike the other strategies in this series, the stock portfolio is always invested - there is no entry/exit signal on the stock side.
Hedge trigger. Each day, the 5-day moving average of VIXY's closing price is compared against its 60-day upper Bollinger Band (60-day mean + 1.5 × 60-day std). When the 5-day MA is at or above the upper band, the signal fires: VIXY is in an elevated volatility regime and is expected to continue rising.
Hedge leg. On signal days, a long VIXY position is entered at today's close and exited at the next day's close (forward-shifted return). The hedge is sized at 50% of the portfolio.
Combined return. (stock_return + hedge_return) / (1 + hedge_ratio) - dividing by 1.5 normalises for the fact that total exposure is 150% on hedge days.
The chart below shows the VIXY price alongside the upper band trigger and signal activations.
plt.figure(figsize=(8, 4.5))
if not vixy_df.empty:
plt.plot(
vixy_df.index,
vixy_df['close'],
color=FL_AMBER,
linewidth=1.4,
label='VIXY close'
)
plt.plot(
vixy_df.index,
vixy_df['vixy_upper'],
color=FL_RED,
linewidth=1.0,
linestyle='--',
label='Upper band (60d mean + 1.5σ)'
)
plt.plot(
vixy_df.index,
vixy_df['vixy_ma'],
color=FL_SLATE,
linewidth=1.0,
linestyle=':',
label=f'{VIXY_SIGNAL_MA}-day MA'
)
signal_on = vixy_df['signal'] == 1
ymin, ymax = plt.ylim()
plt.fill_between(
vixy_df.index,
ymin,
vixy_df['close'].max() * 1.05,
where=signal_on,
alpha=0.08,
color=FL_RED,
label='Hedge active'
)
plt.ylabel('VIXY price ($)')
plt.title('VIXY price, trigger band, and hedge activation periods')
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.gca().xaxis.set_major_locator(mdates.YearLocator())
plt.legend(loc='upper right', fontsize=9)
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
plt.figure(figsize=(8, 4.5))
if not vixy_signal.empty:
plt.fill_between(vixy_signal.index, vixy_signal.values, alpha=0.4, color=FL_RED)
plt.plot(vixy_signal.index, vixy_signal.values, color=FL_RED, linewidth=1.0)
plt.ylabel('Signal')
plt.ylim(-0.05, 1.3)
plt.yticks([0, 1], ['Off', 'On'])
plt.title('Hedge signal state over time')
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.gca().xaxis.set_major_locator(mdates.YearLocator())
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
Hedge activation frequency
How often the VIXY hedge was active over the backtest period. A high hedge frequency may erode returns on calm days when VIXY drifts lower.
if not vixy_signal.empty:
total_days = vixy_signal.notna().sum()
active_days = int(vixy_signal.sum())
inactive_days = total_days - active_days
active_pct = active_days / total_days if total_days > 0 else 0
freq_df = pd.DataFrame({
'State': ['Hedge active (VIXY long)', 'Hedge inactive (stocks only)'],
'Days': [active_days, inactive_days],
'Share': [active_pct, 1 - active_pct],
})
plt.figure(figsize=(8, 4.5))
bar_colors = ['#ef4444', FL_SLATE]
bars = plt.barh(
freq_df['State'],
freq_df['Days'],
color=bar_colors,
height=0.45
)
for bar, (_, row) in zip(bars, freq_df.iterrows()):
plt.text(
bar.get_width() + 5,
bar.get_y() + bar.get_height() / 2,
f"{row['Share']:.1%}",
va='center',
fontsize=10,
color=FL_TEXT2
)
plt.xlabel('Number of days')
plt.title('Hedge activation frequency')
plt.tick_params(axis='both', which='both', length=0)
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
freq_display_df = freq_df.copy()
freq_display_df['Days'] = freq_display_df['Days'].map(lambda v: f'{int(v):,}')
freq_display_df['Share'] = freq_display_df['Share'].map(fmt_pct)
display(freq_display_df)
| State | Days | Share | |
|---|---|---|---|
| 0 | Hedge active (VIXY long) | 117 | 5.55% |
| 1 | Hedge inactive (stocks only) | 1,992 | 94.45% |
Performance summary
All three strategies start from $100,000 over the same period.
- CAGR: compound annual growth rate from start to end equity
- Sharpe: annualised mean daily return / standard deviation × sqrt(252), no risk-free rate
- Max drawdown: largest peak to current loss in dollar terms
The hedge is expected to reduce max drawdown during stress periods at the cost of some upside on calm days when the VIXY position decays.
fmt = {
'Ending Equity': fmt_dollar,
'Total PnL': fmt_dollar,
'Max Drawdown': fmt_dollar,
'CAGR': fmt_pct,
'Sharpe': fmt_f2,
}
cols = ['Strategy', 'Start', 'End', 'Ending Equity', 'Total PnL', 'Max Drawdown', 'CAGR', 'Sharpe']
summary_view = summary_df[cols].copy()
for col, fn in fmt.items():
summary_view[col] = summary_view[col].map(fn)
display(summary_view.T)
| 0 | 1 | 2 | |
|---|---|---|---|
| Strategy | Selective Long VIXY Hedge | Equal Weighted Buy and Hold | QQQ Buy and Hold |
| Start | 2018-01-02 | 2018-01-03 | 2018-01-03 |
| End | 2026-05-22 | 2026-05-22 | 2026-05-22 |
| Ending Equity | $ 613,746 | $ 1,199,878 | $ 478,324 |
| Total PnL | $ 513,746 | $ 1,099,878 | $ 378,324 |
| Max Drawdown | $ -121,687 | $ -237,926 | $ -92,011 |
| CAGR | 24.16% | 34.51% | 20.53% |
| Sharpe | 1.04 | 1.19 | 0.91 |
Equity curves
Portfolio value over time from $100,000 starting capital. Selective Long VIXY Hedge (blue) vs equal-weight buy-and-hold (slate) and QQQ (amber).
plt.figure(figsize=(8, 4.5))
for name, group in comparison_equity_df.groupby('Strategy'):
plt.plot(
group['date'],
group['equity'],
label=name,
color=COLORS.get(name, '#333'),
linewidth=LWIDTHS.get(name, 1.5),
linestyle='-' if name == STRATEGY_NAME else '--',
)
plt.ylabel('Portfolio equity ($)')
plt.title('Portfolio equity from $100,000 starting capital')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda y, _: f'${y:,.0f}')
)
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.gca().xaxis.set_major_locator(mdates.YearLocator())
plt.legend(loc='upper left')
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
equity_end_df = (
comparison_equity_df
.sort_values('date')
.groupby('Strategy')
.tail(1)[['Strategy', 'date', 'equity']]
.copy()
)
equity_end_df['date'] = equity_end_df['date'].dt.strftime('%Y-%m-%d')
equity_end_df['equity'] = equity_end_df['equity'].map(lambda v: f'${v:,.0f}')
equity_end_df.columns = ['Strategy', 'End date', 'Ending equity']
display(equity_end_df)
| Strategy | End date | Ending equity | |
|---|---|---|---|
| 4216 | Equal Weighted Buy and Hold | 2026-05-22 | $1,199,878 |
| 2108 | Selective Long VIXY Hedge | 2026-05-22 | $613,746 |
| 6324 | QQQ Buy and Hold | 2026-05-22 | $478,324 |
Drawdown
Dollar distance from each strategy's running equity peak.
plt.figure(figsize=(8, 4.5))
for name, group in drawdown_comparison_df.groupby('Strategy'):
plt.plot(
group['date'],
group['drawdown'],
label=name,
color=COLORS.get(name, '#333'),
linewidth=LWIDTHS.get(name, 1.5),
linestyle='-' if name == STRATEGY_NAME else '--',
)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.ylabel('Drawdown ($)')
plt.title('Drawdown from equity peak')
plt.gca().yaxis.set_major_formatter(
mticker.FuncFormatter(lambda y, _: f'${y:,.0f}')
)
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.gca().xaxis.set_major_locator(mdates.YearLocator())
plt.legend(loc='lower left')
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()
drawdown_summary_df = (
drawdown_comparison_df
.groupby('Strategy', as_index=False)['drawdown']
.min()
.copy()
)
drawdown_summary_df['drawdown'] = drawdown_summary_df['drawdown'].map(lambda v: f'${v:,.0f}')
drawdown_summary_df.columns = ['Strategy', 'Worst drawdown']
display(drawdown_summary_df)
| Strategy | Worst drawdown | |
|---|---|---|
| 0 | Equal Weighted Buy and Hold | $-237,926 |
| 1 | QQQ Buy and Hold | $-92,011 |
| 2 | Selective Long VIXY Hedge | $-121,687 |