| Overall Statistics |
|
Total Orders 477 Average Win 2.73% Average Loss -1.59% Compounding Annual Return -81.431% Drawdown 59.000% Expectancy -0.096 Start Equity 25000 End Equity 12517.72 Net Profit -49.929% Sharpe Ratio -1.162 Sortino Ratio -1.2 Probabilistic Sharpe Ratio 2.392% Loss Rate 67% Win Rate 33% Profit-Loss Ratio 1.71 Alpha -0.665 Beta 1.43 Annual Standard Deviation 0.59 Annual Variance 0.348 Information Ratio -1.342 Tracking Error 0.5 Treynor Ratio -0.479 Total Fees $218.72 Estimated Strategy Capacity $7500000.00 Lowest Capacity Asset SPY 32RWGFXRB1E92|SPY R735QTJ8XC9X Portfolio Turnover 178.39% |
from AlgorithmImports import *
import numpy as np
from datetime import datetime, timedelta
class DailyStrangleVIXStrategy(QCAlgorithm):
def Initialize(self):
# Set start and end dates
self.SetStartDate(2025, 1, 1)
self.SetEndDate(2025, 6, 1)
# Set cash
self.SetCash(25000)
# Add equity data for SPY (our underlying)
self.spy = self.add_equity("SPY", Resolution.Minute)
self.spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
# Add VIX data
self.vix = self.add_data(CBOE, "VIX", Resolution.Daily)
# Add option data for SPY
option = self.add_option("SPY", Resolution.Minute)
self.spy_option = option.Symbol
# Set option filter
option.SetFilter(self.OptionFilter)
# Initialize variables
self.current_strangle = None
self.entry_time = None
self.vix_threshold = 20 # VIX threshold for favorable conditions
self.spy_volatility_window = RollingWindow[float](20) # 20-day rolling window for SPY volatility
self.spy_price_window = RollingWindow[float](21) # 21 days for volatility calculation
self.vix_ma_window = RollingWindow[float](10) # 10-day VIX moving average
# Risk management for long strangle
self.max_position_size = 0.05 # Max 5% of portfolio per trade (buying premium)
self.profit_target = 1.0 # 100% profit target
self.stop_loss = -0.5 # 50% stop loss (half premium paid)
# Track daily P&L
self.daily_pnl = 0
self.trade_count = 0
# Schedule functions
self.schedule.on(self.date_rules.every_day("SPY"),
self.time_rules.at(9, 35),
self.CheckAndOpenStrangle)
self.schedule.on(
self.date_rules.every_day("SPY"),
self.time_rules.before_market_close("SPY", 10),
self.CloseStrangle
)
self.schedule.on(
self.date_rules.every_day("SPY"),
self.time_rules.before_market_close("SPY", 5),
self.close_of_day
)
# debug and logging
self.SetWarmUp(25) # Warm up period for indicators
def OptionFilter(self, universe):
return universe.IncludeWeeklys().Strikes(-10, 10).Expiration(0, 7)
def OnData(self, data):
# Update price windows
if self.spy.Symbol in data and data[self.spy.Symbol] is not None:
self.spy_price_window.Add(data[self.spy.Symbol].Close)
# Update VIX data
if self.vix.Symbol in data and data[self.vix.Symbol] is not None:
vix_value = data[self.vix.Symbol].Value
self.vix_ma_window.Add(vix_value)
# Calculate SPY realized volatility if we have enough data
if self.spy_price_window.IsReady:
returns = []
for i in range(1, self.spy_price_window.Count):
if self.spy_price_window[i] != 0:
daily_return = np.log(self.spy_price_window[i-1] / self.spy_price_window[i])
returns.append(daily_return)
if len(returns) >= 19: # Need at least 19 returns for 20-day volatility
realized_vol = np.std(returns) * np.sqrt(252) * 100 # Annualized volatility in %
self.spy_volatility_window.Add(realized_vol)
def CheckAndOpenStrangle(self):
"""Check conditions and open strangle if favorable"""
if self.is_warming_up:
return
# Don't open new position if we already have one
if self.current_strangle is not None:
return
# Check if we have the required data
if not self.vix_ma_window.IsReady or not self.spy_volatility_window.IsReady:
return
# Get current VIX and its moving average
current_vix = self.vix_ma_window[0]
vix_ma = sum(self.vix_ma_window) / self.vix_ma_window.Count
# Get current realized volatility
current_realized_vol = self.spy_volatility_window[0]
avg_realized_vol = sum(self.spy_volatility_window) / self.spy_volatility_window.Count
# Define favorable conditions for strangle
favorable_conditions = self.CheckFavorableConditions(current_vix, vix_ma,
current_realized_vol, avg_realized_vol)
self.OpenStrangle()
def CheckFavorableConditions(self, current_vix, vix_ma, current_realized_vol, avg_realized_vol):
"""
Check if conditions are favorable for opening a strangle
Returns True if conditions suggest high probability of movement
"""
conditions_met = 0
total_conditions = 0
# Condition 1: VIX is elevated (above threshold)
total_conditions += 1
if current_vix > self.vix_threshold:
conditions_met += 1
self.debug(f"✓ VIX elevated: {current_vix:.2f} > {self.vix_threshold}")
# Condition 2: VIX is above its recent average (trending up)
total_conditions += 1
if current_vix > vix_ma * 1.05: # 5% above average
conditions_met += 1
self.debug(f"✓ VIX above MA: {current_vix:.2f} > {vix_ma:.2f}")
# Condition 3: Realized volatility is picking up
total_conditions += 1
if current_realized_vol > avg_realized_vol * 1.1: # 10% above average
conditions_met += 1
self.debug(f"✓ Realized vol elevated: {current_realized_vol:.2f} > {avg_realized_vol:.2f}")
# Condition 4: VIX term structure (if available) - simplified check
total_conditions += 1
if current_vix > 15: # Basic threshold indicating some market stress
conditions_met += 1
self.debug(f"✓ VIX shows market stress: {current_vix:.2f}")
# Require at least 2 out of 4 conditions
is_favorable = conditions_met >= 3
self.debug(f"Conditions check: {conditions_met}/{total_conditions} met. Favorable: {is_favorable}")
self.debug(f"VIX: {current_vix:.2f}, VIX MA: {vix_ma:.2f}, RV: {current_realized_vol:.2f}, RV MA: {avg_realized_vol:.2f}")
return is_favorable
def OpenStrangle(self):
"""Open a strangle position"""
try:
# Get option chain
option_chain = self.CurrentSlice.OptionChains.get(self.spy_option)
if option_chain is None or len(option_chain) == 0:
return
# Get current SPY price
spy_price = self.Securities[self.spy.Symbol].Price
# Find the best strikes for strangle
target_call_strike = spy_price
target_put_strike = spy_price
# Find closest options to target strikes
calls = [x for x in option_chain if x.Right == OptionRight.Call]
puts = [x for x in option_chain if x.Right == OptionRight.Put]
if len(calls) == 0 or len(puts) == 0:
return
# Select the call closest to target strike
best_call = min(calls, key=lambda x: abs(x.Strike - target_call_strike))
# Select the put closest to target strike
best_put = min(puts, key=lambda x: abs(x.Strike - target_put_strike))
# Calculate position size based on premium and risk management
call_premium = best_call.AskPrice
put_premium = best_put.AskPrice
total_premium = call_premium + put_premium
if total_premium <= 0:
return
# Calculate maximum contracts based on premium cost (we're buying)
max_risk = self.Portfolio.TotalPortfolioValue * self.max_position_size
max_contracts = int(max_risk / (total_premium * 100)) # 100 shares per contract
if max_contracts <= 0:
return
# Limit to reasonable size
contracts = min(max_contracts, 10)
# Execute the trades - BUY the strangle to profit from movement
call_order = self.market_order(best_call.Symbol, -contracts) # Buy call
put_order = self.market_order(best_put.Symbol, -contracts) # Buy put
# Store strangle information
self.current_strangle = {
'call_symbol': best_call.Symbol,
'put_symbol': best_put.Symbol,
'call_strike': best_call.Strike,
'put_strike': best_put.Strike,
'contracts': contracts,
'entry_premium': total_premium,
'entry_time': self.Time,
'call_order': call_order,
'put_order': put_order
}
self.entry_time = self.Time
self.trade_count += 1
self.debug(f"Opened strangle #{self.trade_count}: Call {best_call.Strike}, Put {best_put.Strike}, " +
f"Contracts: {contracts}, Premium: ${total_premium:.2f}")
except Exception as e:
self.debug(f"Error opening strangle: {str(e)}")
def CloseStrangle(self):
"""Close the current strangle position"""
if self.current_strangle is None:
return
try:
call_symbol = self.current_strangle['call_symbol']
put_symbol = self.current_strangle['put_symbol']
contracts = self.current_strangle['contracts']
# Close positions - Sell back the long strangle
self.market_order(call_symbol, contracts) # Sell call
self.market_order(put_symbol, contracts) # Sell put
# Calculate P&L
call_position = self.Portfolio[call_symbol]
put_position = self.Portfolio[put_symbol]
total_pnl = call_position.UnrealizedProfit + put_position.UnrealizedProfit
self.debug(f"Closed strangle #{self.trade_count}: P&L: ${total_pnl:.2f}")
# Reset strangle
self.current_strangle = None
self.entry_time = None
except Exception as e:
self.debug(f"Error closing strangle: {str(e)}")
def OnOrderEvent(self, orderEvent):
"""Handle order events"""
if orderEvent.Status == OrderStatus.Filled:
self.debug(f"Order filled: {orderEvent.Symbol} {orderEvent.FillQuantity} @ ${orderEvent.FillPrice}")
def close_of_day(self):
"""End of day processing"""
# Force close any open positions
if self.current_strangle is not None:
self.CloseStrangle()
# Log daily performance
self.debug(f"End of day - Portfolio value: ${self.Portfolio.TotalPortfolioValue:.2f}")