| Overall Statistics |
|
Total Orders 114 Average Win 0.00% Average Loss -0.23% Compounding Annual Return 0.324% Drawdown 1.700% Expectancy -0.383 Start Equity 500000 End Equity 500413 Net Profit 0.083% Sharpe Ratio -2.051 Sortino Ratio -1.961 Probabilistic Sharpe Ratio 29.247% Loss Rate 39% Win Rate 61% Profit-Loss Ratio 0.01 Alpha -0.054 Beta 0.015 Annual Standard Deviation 0.024 Annual Variance 0.001 Information Ratio -3.375 Tracking Error 0.091 Treynor Ratio -3.24 Total Fees $0.00 Estimated Strategy Capacity $36000.00 Lowest Capacity Asset SPXW 32953BE0XRU6M|SPX 31 Portfolio Turnover 0.09% |
# region imports
from AlgorithmImports import *
from datetime import datetime
# endregion
class IVSkewStraddleStrategy(QCAlgorithm):
def Initialize(self):
# Limit backtest to one month for memory optimization
self.SetStartDate(2023, 5, 1) # Start date
self.SetEndDate(2023, 8, 1) # End date
self.SetCash(500000) # Starting cash
# Add SPX index
index = self.AddIndex("SPX")
# Add SPXW options (weekly options for SPX)
option = self.AddIndexOption(index.Symbol, "SPXW")
option.SetFilter(lambda u: u.IncludeWeeklys().Expiration(0, 0).Strikes(-30, 30))
option.PriceModel = OptionPriceModels.BinomialCoxRossRubinstein()
self.option_symbol = option.Symbol
# Threshold for IV skew steepness
self.iv_slope_threshold = 0.005
# Risk management parameters
self.max_portfolio_risk = 0.2 # 10% max risk per trade
self.profit_target = 0.5 # 50% profit target
self.stop_loss = 0.5 # 50% stop loss
self.trade_open = False
self.initial_debit = 0
# Schedule the processing at market open
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.AfterMarketOpen("SPX", 1),
self.ProcessMarketOpen
)
def ProcessMarketOpen(self):
if self.trade_open:
return # Only one trade per day
# Get the option chain
chain = self.CurrentSlice.OptionChains.get(self.option_symbol, None)
if not chain:
self.Log("Option chain not available at market open.")
return
# Calculate IV skew data
iv_skew_data = self.CalculateSteepestIVSkew(chain)
if iv_skew_data is None:
self.Log("IV skew data is None at market open.")
return
steepest_slope = iv_skew_data["steepest_slope"]
avg_iv = iv_skew_data["average_iv"]
self.Log(f"Date: {self.Time.date()} | Steepest Local Slope of Put IV Skew at Market Open: {steepest_slope}")
self.Log(f"Date: {self.Time.date()} | Average IV of Puts at Market Open: {avg_iv}")
if steepest_slope < self.iv_slope_threshold:
self.Log("Steepest slope below threshold at market open. Skipping trade.")
return
# Execute straddle trade with hedge
expiry = iv_skew_data["expiry"]
atm_strike = iv_skew_data["atm_strike"]
# Build the straddle strategy (buying ATM call and put)
straddle = OptionStrategies.Straddle(self.option_symbol, atm_strike, expiry)
self.Buy(straddle, 1)
# Hedge with 15% OTM put (selling the put)
otm_put = iv_skew_data["otm_put"]
self.Sell(otm_put.Symbol, 1)
# Calculate initial debit (total cost)
initial_debit = self.CalculateInitialDebit(atm_strike, expiry, otm_put, chain)
if initial_debit is None:
self.Log("Unable to calculate initial debit. Skipping trade.")
return
self.initial_debit = initial_debit
self.Log(f"Opened Straddle at strike {atm_strike} with hedge put at market open. Initial debit: ${self.initial_debit:.2f}")
self.trade_open = False
def OnData(self, data):
if self.trade_open:
self.CheckPositionManagement()
def CheckPositionManagement(self):
# Calculate total PnL of open positions
total_pnl = sum([holding.UnrealizedProfit for holding in self.Portfolio.Values if holding.Invested])
# Profit target reached
if total_pnl >= self.initial_debit * self.profit_target:
self.Liquidate()
self.Log(f"Closed position at profit target on {self.Time}. Profit: ${total_pnl:.2f}")
self.trade_open = False
# Stop loss reached
elif total_pnl <= -self.initial_debit * self.stop_loss:
self.Liquidate()
self.Log(f"Closed position at stop loss on {self.Time}. Loss: ${total_pnl:.2f}")
self.trade_open = False
def CalculateSteepestIVSkew(self, chain):
"""
Calculate the steepest local slope of the put IV skew (IV vs Strike Price)
and the average IV of the puts.
"""
puts = [
c for c in chain
if c.Right == OptionRight.Put and c.ImpliedVolatility > 0 and c.ImpliedVolatility <= 5
]
if len(puts) < 2:
self.Log("Not enough puts to calculate steepest slope.")
return None # Not enough data to calculate slope
# Calculate average IV
avg_iv = sum(c.ImpliedVolatility for c in puts) / len(puts)
# Sort puts by strike price
puts = sorted(puts, key=lambda c: c.Strike)
# Calculate local slopes
steepest_slope = 0
for i in range(len(puts) - 1):
strike_diff = puts[i + 1].Strike - puts[i].Strike
iv_diff = puts[i + 1].ImpliedVolatility - puts[i].ImpliedVolatility
if strike_diff != 0: # Avoid division by zero
local_slope = abs(iv_diff / strike_diff)
if local_slope > steepest_slope:
steepest_slope = local_slope
expiry = puts[0].Expiry
underlying_price = chain.Underlying.Price
# Find ATM strike
atm_strike = min([c.Strike for c in puts], key=lambda x: abs(x - underlying_price))
# Find 15% OTM put
otm_put = min(
puts,
key=lambda c: abs(c.Strike - 0.85 * underlying_price)
)
return {
"steepest_slope": steepest_slope,
"average_iv": avg_iv,
"expiry": expiry,
"atm_strike": atm_strike,
"otm_put": otm_put
}
def CalculateInitialDebit(self, atm_strike, expiry, otm_put, chain):
"""
Calculate the initial debit (total cost) of entering the position.
"""
# Get ATM call and put contracts
atm_call = [
c for c in chain
if c.Right == OptionRight.Call and c.Strike == atm_strike and c.Expiry == expiry
]
atm_put = [
c for c in chain
if c.Right == OptionRight.Put and c.Strike == atm_strike and c.Expiry == expiry
]
if not atm_call or not atm_put:
self.Log("ATM contracts not found.")
return None
# Get option prices
atm_call_price = atm_call[0].AskPrice
atm_put_price = atm_put[0].AskPrice
otm_put_price = otm_put.BidPrice
# Calculate initial debit (cost to buy straddle minus premium received from selling OTM put)
initial_debit = ((atm_call_price + atm_put_price) - otm_put_price) * 100 # Multiply by 100 for option multiplier
return initial_debit