| Overall Statistics |
|
Total Orders 167 Average Win 0.78% Average Loss -0.27% Compounding Annual Return 2.542% Drawdown 4.400% Expectancy 1.066 Start Equity 10000000 End Equity 10254227.5 Net Profit 2.542% Sharpe Ratio -0.107 Sortino Ratio -0.128 Probabilistic Sharpe Ratio 22.576% Loss Rate 47% Win Rate 53% Profit-Loss Ratio 2.88 Alpha -0.013 Beta -0.178 Annual Standard Deviation 0.049 Annual Variance 0.002 Information Ratio 0.238 Tracking Error 0.172 Treynor Ratio 0.03 Total Fees $1939.00 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY 30Z7S6S7CKC86|SPY R735QTJ8XC9X Portfolio Turnover 1.54% |
# Merged version of VolatilitySurfaceMomentum and MultiAssetStraddle strategies (Option 2: Parallel Execution)
from AlgorithmImports import *
from scipy.interpolate import SmoothBivariateSpline
from sklearn.decomposition import PCA
from datetime import timedelta
import numpy as np
from collections import deque
class IntegratedVolStraddle(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 1, 1)
self.SetEndDate(2019, 1, 1)
self.SetCash(10_000_000)
self.tickers = ["AAPL", "TSLA", "NVDA", "AMZN", "SPY"]
self.under = {}
self.option_symbol = {}
self.positions = {}
self.entryCost = {}
self.entryExpiry = {}
self.entrySpot = {}
self.lastEntryTime = {}
self.positionSource = {} # track which strategy opened it
# Strategy-specific parameters
self.atr = {}
self.macd = {}
self.rsi = {}
self.atrMultiplier = 1.5
self.takeProfit = 0.4
self.stopLoss = -0.05
self.cooldown_days = 30
self.max_trade_value = 50_000
# IV surface momentum data
self.spline = {}
self.splineHist = {t: deque(maxlen=5) for t in self.tickers}
self.hist = {f: {t: deque(maxlen=250) for t in self.tickers}
for f in ("level_change", "rolling_level", "skew_change", "smile_change")}
self.q = {
"level_change": 0.6448,
"rolling_level": 0.6732,
"smile_change": 0.5417,
"skew_change": 0.6163
}
self.SetWarmUp(200)
for t in self.tickers:
eq = self.AddEquity(t, Resolution.Daily).Symbol
op = self.AddOption(t, Resolution.Daily)
op.SetFilter(lambda u: u.IncludeWeeklys().Strikes(-1, 1).Expiration(30, 45))
self.under[t] = eq
self.option_symbol[t] = op.Symbol
self.atr[t] = self.ATR(eq, 14, Resolution.Daily)
self.macd[t] = self.MACD(eq, 12, 26, 9, MovingAverageType.Wilders, Resolution.Daily)
self.rsi[t] = self.RSI(eq, 14, MovingAverageType.Wilders, Resolution.Daily)
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen("SPY", 10), self.FitSurfaces)
def FitSurfaces(self):
for t, sym in self.option_symbol.items():
chain = self.OptionChain(sym)
if not chain:
continue
c = [x for x in chain if x.ImpliedVolatility > 0 and x.UnderlyingLastPrice > 0]
if len(c) < 15:
continue
try:
K = [x.Strike for x in c]
S = [x.UnderlyingLastPrice for x in c]
M = [k / s - 1 for k, s in zip(K, S)]
DTE = [(x.Expiry.date() - self.Time.date()).days for x in c]
IV = [x.ImpliedVolatility for x in c]
spl = SmoothBivariateSpline(M, DTE, IV, kx=3, ky=3, s=25)
self.spline[t] = spl
self.splineHist[t].append(spl)
except Exception as e:
self.Debug(f"{t} spline err {e}")
def OnData(self, slice: Slice):
if self.IsWarmingUp:
return
for t in self.tickers:
if self.HasOpenStraddle(t):
self._exit_checks(t)
continue
if self.lastEntryTime.get(t) and (self.Time - self.lastEntryTime[t]).days < self.cooldown_days:
continue
# MAS entry condition
if self._atr_breakout(t) and self._macd_cross(t) and self._rsi_neutral(t):
self._enter_straddle(t, slice, "MAS")
# VSM entry condition
if self._vsm_trigger(t):
self._enter_straddle(t, slice, "VSM")
def _atr_breakout(self, t):
return self.atr[t].IsReady and (self.Securities[t].Close - self.Securities[t].Low) > self.atrMultiplier * self.atr[t].Current.Value
def _macd_cross(self, t):
return self.macd[t].IsReady and self.macd[t].Current.Value > self.macd[t].Signal.Current.Value
def _rsi_neutral(self, t):
return self.rsi[t].IsReady and 35 < self.rsi[t].Current.Value < 65
def _vsm_trigger(self, t):
if t not in self.spline or len(self.splineHist[t]) < 4:
return False
try:
splines = list(self.splineHist[t])[-4:]
m = np.linspace(-0.25, 0.25, 11)
iv = [np.array([s.ev(x, 30) for x in m]) for s in splines]
pca = PCA(3)
pca.fit(iv[:3])
pc1 = pca.transform([iv[1]])[0]
pc2 = pca.transform([iv[2]])[0]
pc3 = pca.transform([iv[3]])[0]
d = pc2 - pc1
lvl, skw, sml = d
self.hist["level_change"][t].append(abs(lvl))
self.hist["skew_change"][t].append(abs(skw))
self.hist["smile_change"][t].append(abs(sml))
rollLvl = np.mean(list(self.hist["level_change"][t])[-3:])
self.hist["rolling_level"][t].append(rollLvl)
qok = lambda f, val: abs(val) > np.quantile(self.hist[f][t], self.q[f])
κ = np.mean(np.gradient(np.gradient(iv[-1], m), m) - np.gradient(np.gradient(iv[-2], m), m))
return all([qok("level_change", lvl), qok("rolling_level", rollLvl), qok("skew_change", skw), qok("smile_change", sml)]) and abs(κ) > 0.01
except:
return False
def _enter_straddle(self, t, slice, source):
chain = slice.OptionChains.get(self.option_symbol[t])
if not chain:
return
contracts = [c for c in chain if c.Expiry > self.Time]
if not contracts:
return
expiry = sorted({c.Expiry for c in contracts})[0]
spot = self.Securities[t].Price
atm_strike = min(contracts, key=lambda c: abs(c.Strike - spot)).Strike
calls = [c for c in contracts if c.Strike == atm_strike and c.Right == OptionRight.Call and c.AskPrice > 0 and c.Volume > 0 and c.Expiry == expiry]
puts = [c for c in contracts if c.Strike == atm_strike and c.Right == OptionRight.Put and c.AskPrice > 0 and c.Volume > 0 and c.Expiry == expiry]
if not calls or not puts:
return
call = sorted(calls, key=lambda c: (-c.Volume, c.AskPrice))[0]
put = sorted(puts, key=lambda c: (-c.Volume, c.AskPrice))[0]
mult = self.Securities[call.Symbol].SymbolProperties.ContractMultiplier
price = (call.AskPrice + put.AskPrice) * mult
if price == 0:
return
qty = int(self.max_trade_value / price)
totalCost = qty * price
if qty < 1 or totalCost > self.Portfolio.Cash * 0.9:
return
self.MarketOrder(call.Symbol, qty)
self.MarketOrder(put.Symbol, qty)
self.entryCost[t] = totalCost
self.entryExpiry[t] = expiry
self.entrySpot[t] = spot
self.lastEntryTime[t] = self.Time
self.positionSource[t] = source
self.Debug(f"[{source}] Entered {t} straddle | Spot: {spot:.2f}, Qty: {qty}")
def _exit_checks(self, t):
positions = [h for h in self.Portfolio.Values if h.Invested and h.Symbol.SecurityType == SecurityType.Option and h.Symbol.Underlying.Value == t]
if not positions:
return
unreal = sum(h.UnrealizedProfit for h in positions)
cost = self.entryCost.get(t, 1)
pl_pct = unreal / cost
days_to_expiry = (self.entryExpiry[t].date() - self.Time.date()).days
if pl_pct >= self.takeProfit or pl_pct <= self.stopLoss or days_to_expiry <= 1:
self.Debug(f"[EXIT] {t} | PnL: {pl_pct:.2%}, DTE: {days_to_expiry}")
self.Liquidate(t)
def HasOpenStraddle(self, t):
return any(h.Invested and h.Symbol.SecurityType == SecurityType.Option and h.Symbol.Underlying.Value == t for h in self.Portfolio.Values)