the IV rank problem #
my original algo only sells premium when IV rank > 40
IV rank = where current IV sits relative to its 52-week range
formula: (current_IV - 52_week_low) / (52_week_high - 52_week_low) * 100
if IV rank is 60, that means current IV is at 60th percentile of its annual range
problem: IV rank alone doesn’t tell you if premium is actually rich
example from last week:
- march 20: IV rank was 42, but actual IV was only 14
- march 21: IV rank was 41, but actual IV was 22
both trades qualified (IV rank > 40) but march 21 had way better premium despite similar IV rank
i was leaving money on the table
the upgrade #
added two more filters on top of IV rank:
- absolute IV threshold: current IV must be > 18
- IV percentile: current IV must be in top 30% vs 90-day rolling average
here’s the implementation:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class VolatilityFilter:
"""
Enhanced volatility filtering for premium selling
Combines IV rank, absolute IV, and IV percentile
"""
def __init__(self, min_iv_rank=40, min_absolute_iv=18, min_iv_percentile=70):
self.min_iv_rank = min_iv_rank
self.min_absolute_iv = min_absolute_iv
self.min_iv_percentile = min_iv_percentile
# cache for IV history
self.iv_history = []
def calculate_iv_rank(self, current_iv, iv_history_52w):
"""
Calculate IV rank (52-week)
Args:
current_iv: Current implied volatility
iv_history_52w: Series of IV values over past 52 weeks
Returns:
IV rank (0-100)
"""
iv_min = iv_history_52w.min()
iv_max = iv_history_52w.max()
if iv_max == iv_min:
return 50.0 # neutral if no range
iv_rank = ((current_iv - iv_min) / (iv_max - iv_min)) * 100
return iv_rank
def calculate_iv_percentile(self, current_iv, iv_history_90d):
"""
Calculate what percentile current IV is vs 90-day history
Args:
current_iv: Current IV
iv_history_90d: Series of IV values over past 90 days
Returns:
Percentile (0-100)
"""
below_current = (iv_history_90d < current_iv).sum()
total_days = len(iv_history_90d)
percentile = (below_current / total_days) * 100
return percentile
def should_sell_premium(self, current_iv, iv_52w, iv_90d):
"""
Determine if conditions are right to sell premium
Args:
current_iv: Current IV level
iv_52w: 52-week IV history
iv_90d: 90-day IV history
Returns:
tuple: (should_sell: bool, reason: str, metrics: dict)
"""
# calculate all metrics
iv_rank = self.calculate_iv_rank(current_iv, iv_52w)
iv_percentile = self.calculate_iv_percentile(current_iv, iv_90d)
metrics = {
'current_iv': current_iv,
'iv_rank': iv_rank,
'iv_percentile': iv_percentile,
'52w_min': iv_52w.min(),
'52w_max': iv_52w.max(),
'90d_avg': iv_90d.mean()
}
# check all three filters
checks = {
'iv_rank': iv_rank >= self.min_iv_rank,
'absolute_iv': current_iv >= self.min_absolute_iv,
'iv_percentile': iv_percentile >= self.min_iv_percentile
}
# all must pass
should_sell = all(checks.values())
# build reason string
if should_sell:
reason = f"✅ All filters passed - IV rank {iv_rank:.1f}, Absolute IV {current_iv:.1f}, Percentile {iv_percentile:.1f}"
else:
failed = [k for k, v in checks.items() if not v]
reason = f"❌ Failed: {', '.join(failed)}"
return should_sell, reason, metrics
def get_premium_quality_score(self, current_iv, iv_52w, iv_90d):
"""
Score from 0-100 indicating how attractive premium is
Higher = better opportunity
"""
iv_rank = self.calculate_iv_rank(current_iv, iv_52w)
iv_percentile = self.calculate_iv_percentile(current_iv, iv_90d)
# normalize absolute IV (assume 10-40 range, clip extremes)
iv_normalized = np.clip((current_iv - 10) / 30, 0, 1) * 100
# weighted average of all three
score = (
iv_rank * 0.4 + # 40% weight on IV rank
iv_normalized * 0.3 + # 30% weight on absolute IV
iv_percentile * 0.3 # 30% weight on IV percentile
)
return score
# integration with main strategy
class SPXCreditSpreadStrategy(bt.Strategy):
"""
Credit spread strategy with enhanced volatility filtering
"""
params = (
('min_iv_rank', 40),
('min_absolute_iv', 18),
('min_iv_percentile', 70),
('max_positions', 5),
)
def __init__(self):
super().__init__()
self.vol_filter = VolatilityFilter(
min_iv_rank=self.params.min_iv_rank,
min_absolute_iv=self.params.min_absolute_iv,
min_iv_percentile=self.params.min_iv_percentile
)
def next(self):
# get current IV data
current_iv = self.get_current_iv('SPX')
iv_52w = self.get_iv_history(days=365)
iv_90d = self.get_iv_history(days=90)
# check if we should sell premium
should_sell, reason, metrics = self.vol_filter.should_sell_premium(
current_iv, iv_52w, iv_90d
)
if not should_sell:
self.log(f"Skipping - {reason}")
return
# get premium quality score
quality_score = self.vol_filter.get_premium_quality_score(
current_iv, iv_52w, iv_90d
)
self.log(f"Premium quality: {quality_score:.1f}/100")
# only trade if quality score > 60
if quality_score < 60:
self.log(f"Quality score too low: {quality_score:.1f}")
return
# proceed with trade entry
self.log(f"✅ Entering trade - {reason}")
# ... rest of entry logic ...
backtest comparison #
ran 2021-2022 backtest with old vs new filtering:
old filter (IV rank > 40 only):
- trades per year: 240
- win rate: 72%
- annual return: 14.3%
- sharpe: 1.64
new filter (IV rank + absolute IV + percentile):
- trades per year: 186
- win rate: 78%
- annual return: 15.7%
- sharpe: 1.81
fewer trades but way better quality
the new filter caught 4 periods in 2022 where IV rank was elevated but absolute IV was still low (june, september, november, december)
those months i would’ve sold premium for shit credits. new filter kept me out
march performance #
applied new filter retroactively to march:
- march 15: would’ve traded (IV rank 48, abs IV 22, percentile 82) ✅
- march 17: would NOT have traded (IV rank 42, abs IV 16, percentile 55) ❌
- march 20: would NOT have traded (IV rank 41, abs IV 14, percentile 48) ❌
- march 21: would’ve traded (IV rank 45, abs IV 22, percentile 75) ✅
i actually did trade march 17 and march 20 with old filter. both were mediocre:
- march 17: $0.70 credit (low)
- march 20: $0.65 credit (really low)
if i’d had new filter running, i would’ve skipped those and waited for better setups
going live april 1 #
new filter goes into production april 1
expecting fewer trades but higher win rate and better returns per trade
will track actual results and compare to backtest prediction
-AK