| Overall Statistics |
|
Total Orders 342 Average Win 3.87% Average Loss -2.12% Compounding Annual Return 1.737% Drawdown 20.500% Expectancy 0.064 Start Equity 100000 End Equity 111065 Net Profit 11.065% Sharpe Ratio -0.088 Sortino Ratio -0.039 Probabilistic Sharpe Ratio 0.471% Loss Rate 62% Win Rate 38% Profit-Loss Ratio 1.83 Alpha -0.007 Beta -0.034 Annual Standard Deviation 0.114 Annual Variance 0.013 Information Ratio -0.425 Tracking Error 0.211 Treynor Ratio 0.298 Total Fees $0.00 Estimated Strategy Capacity $11000.00 Lowest Capacity Asset SPXW YRGAXIF5MPVY|SPX 31 Portfolio Turnover 0.38% |
# region imports
from AlgorithmImports import *
# endregion
# region imports
from datetime import timedelta
from model import LogisticRegressionModel
# endregion
class TradeDecisionEngine:
"""
Implements the trade decision logic for SPX Long Straddle Strategy.
Uses logistic regression model to determine trade entry conditions.
"""
def __init__(self, algorithm):
"""
Initialize the decision engine with reference to the main algorithm.
Parameters:
- algorithm: QCAlgorithm instance with access to market data
"""
self.algorithm = algorithm
self.model = LogisticRegressionModel()
self.probability_threshold = 0.40 # Exact threshold value
def should_enter_trade(self):
"""
Determine if a trade should be entered based on model probability
and additional filters.
Returns:
- boolean: True if all trade conditions are met, False otherwise
"""
# Get indicator values using required calculations
current_term_slope = self.algorithm.CalculateTermSlope()
current_iv_zscore = self.algorithm.CalculateIVZScore(252) # Must use 252 days
# Get VIX value
current_vix = self.algorithm.Securities[self.algorithm.vix_index_symbol].Price
# Calculate trade probability
z, trade_probability = self.model.calculate_probability(current_term_slope, current_iv_zscore)
# Check conditions with exact thresholds
probability_condition = trade_probability > self.probability_threshold
vix_condition = current_vix >= 21
# Log all conditions with Phase 3 prefix
self.algorithm.CustomLog(f"[PHASE3] Probability calculation: z={z:.6f}, probability={trade_probability:.6f}")
self.algorithm.CustomLog(f"[PHASE3] Conditions: Term Slope={current_term_slope:.6f}, IV Z-Score={current_iv_zscore:.6f}, VIX={current_vix:.2f}")
self.algorithm.CustomLog(f"[PHASE3] VIX filter: {vix_condition} (VIX = {current_vix:.2f})")
self.algorithm.CustomLog(f"[PHASE3] Trade probability: {trade_probability:.6f}, Threshold: {self.probability_threshold}, Decision: {probability_condition and vix_condition}")
# Decision must check both conditions
return probability_condition and vix_condition
def verify_dte(self, option_chain):
"""
Verify that options in the chain have exactly 1 DTE.
Parameters:
- option_chain: Collection of option contracts
Returns:
- boolean: True if valid 1 DTE options exist, False otherwise
"""
if not option_chain:
self.algorithm.CustomLog("[PHASE3] No option chain available to verify DTE")
return False
# Target expiry date should be exactly 1 day from now
target_expiry_date = self.algorithm.Time.date() + timedelta(days=1)
# Filter for contracts expiring tomorrow (1 DTE)
contracts_1dte = [contract for contract in option_chain
if contract.Expiry.date() == target_expiry_date]
has_valid_dte = len(contracts_1dte) > 0
if not has_valid_dte:
self.algorithm.CustomLog(f"[PHASE3] No 1 DTE contracts found, target expiry: {target_expiry_date}")
else:
self.algorithm.CustomLog(f"[PHASE3] Verified 1 DTE options: {len(contracts_1dte)} contracts available")
return has_valid_dte # region imports
from datetime import datetime
from AlgorithmImports import *
# endregion
class TradeExecutionEngine:
"""
Handles position sizing and order execution for SPX Long Straddle Strategy.
Implements exact specifications from Phase 4 of the system design.
"""
def __init__(self, algorithm):
"""Initialize with reference to main algorithm."""
self.algorithm = algorithm
self.active_orders = {} # OrderId -> OrderDetails tracking
def execute_straddle_position(self, atm_options):
"""
Execute a straddle position with exact position sizing rules.
Parameters:
- atm_options: ATM call and put options to trade
"""
# Calculate position size (exactly 20% of portfolio)
portfolio_value = self.algorithm.Portfolio.TotalPortfolioValue
position_value = portfolio_value * 0.20
# Calculate option costs
call_price = atm_options.Call.LastPrice
put_price = atm_options.Put.LastPrice
total_cost = call_price + put_price
# Calculate contracts with hard limit of 1
contracts_by_value = math.floor(position_value / total_cost)
contracts = min(contracts_by_value, 1)
# Log position sizing details
self.algorithm.CustomLog(f"[PHASE4] Position sizing: Portfolio=${portfolio_value:.2f}, "
f"Allocation=${position_value:.2f} (20%), "
f"Option Cost=${total_cost:.2f}, "
f"Contracts calculated={contracts_by_value}, "
f"Final contracts={contracts}")
# If zero contracts calculated but we have enough for 1, proceed with 1
if contracts == 0 and portfolio_value >= total_cost:
self.algorithm.CustomLog("[PHASE4] Warning: Calculated contracts is zero. Attempting to trade 1 contract.")
contracts = 1
# Place market orders for both options
if contracts > 0:
try:
# Place call order
call_order = self.algorithm.MarketOrder(atm_options.Call.Symbol, contracts)
self.active_orders[call_order.OrderId] = OrderDetails(call_order, "Call")
# Place put order
put_order = self.algorithm.MarketOrder(atm_options.Put.Symbol, contracts)
self.active_orders[put_order.OrderId] = OrderDetails(put_order, "Put")
# Log order placement
self.algorithm.CustomLog(
f"[PHASE4] Straddle executed: "
f"Call={atm_options.Call.Symbol}, "
f"Put={atm_options.Put.Symbol}, "
f"Total Cost=${total_cost * contracts:.2f}"
)
except Exception as e:
self.algorithm.CustomLog(f"[PHASE4] Error executing orders: {str(e)}")
else:
self.algorithm.CustomLog("[PHASE4] Insufficient buying power for minimum position")
def on_order_event(self, order_event):
"""Handle order status updates and partial fills."""
if order_event.OrderId in self.active_orders:
order_details = self.active_orders[order_event.OrderId]
# Log order status
self.algorithm.CustomLog(
f"[PHASE4] Order {order_event.OrderId} status: {order_event.Status} "
f"({order_details.option_type})"
)
# Handle partial fills
if order_event.Status == OrderStatus.PartiallyFilled:
self.algorithm.CustomLog(
f"[PHASE4] Partial fill for Order {order_event.OrderId}: "
f"{order_event.FillQuantity}/{order_event.Quantity}"
)
class OrderDetails:
"""Simple struct to track order information."""
def __init__(self, order, option_type):
self.order = order
self.option_type = option_type # "Call" or "Put" # region imports
from AlgorithmImports import *
import numpy as np
import math
from model import LogisticRegressionModel
from decision import TradeDecisionEngine
from execution import TradeExecutionEngine
from trade_management import TradeManagementEngine
# endregion
# Renamed class to SPXLongStraddleStrategy
class SPXLongStraddleStrategy(QCAlgorithm):
def Initialize(self):
"""
Phase 1: Initialize strategy parameters, data subscriptions, and scheduling.
Phase 2: Add warm-up period and initialize storage for historical IVs
Phase 3: Initialize logistic regression model and trade decision engine
Phase 4: Initialize trade execution engine
Phase 5: Initialize trade management engine
"""
self.SetStartDate(2019, 3, 31) # Example Start Date
# self.SetEndDate(2025, 4, 14) # Example End Date
self.SetCash(100000) # Example Cash
# --- Set warm-up period for historical IV calculation ---
self.SetWarmUp(365) # 365 calendar days to ensure at least 252 trading days
# --- Model Coefficients (Phase 3) ---
self.model_intercept = -0.60
self.model_term_slope_coeff = -0.57
self.model_iv_zscore_coeff = 0.36
# self.CustomLog(f"[PHASE3] Model coefficients initialized: Intercept={self.model_intercept}, TermSlope={self.model_term_slope_coeff}, IVZScore={self.model_iv_zscore_coeff}")
# --- Symbol Setup ---
self.spx_index_symbol = self.AddIndex("SPX", Resolution.Minute).Symbol
self.vix_index_symbol = self.AddIndex("VIX", Resolution.Minute).Symbol
# --- Modified Option Setup with Better Filtering ---
self.spx_option = self.AddIndexOption(self.spx_index_symbol, "SPXW", Resolution.Minute)
self.option_symbol = self.spx_option.Symbol
# Set filter for trading options (1 DTE)
self.spx_option.SetFilter(lambda universe: universe
.IncludeWeeklys() # Ensure we are using weeklys (SPXW)
.Expiration(0, 1) # Only 0 and 1 DTE (Contracts expire T+1, so 1 DTE is needed on trade day)
.Strikes(-10, 10)) # Strikes around ATM
# --- Separate Option Chain for IV Calculation (30 DTE) ---
self.iv_option = self.AddIndexOption(self.spx_index_symbol, Resolution.Minute)
self.iv_option_symbol = self.iv_option.Symbol
# Set filter targeting ~30 day options for IV calculation
self.iv_option.SetFilter(lambda universe: universe
.IncludeWeeklys() # Include both standard and weekly options
.Expiration(25, 35) # Target options ~30 days out (25-35 day window)
.Strikes(-1, 1)) # Only closest strikes to ATM for more accurate IV
# --- Phase 2: Historical IV Storage ---
self.historical_iv_values = [] # Store historical IV values
self.last_iv_date = None # Track the last date we calculated IV for
# --- Phase 3: Initialize trade decision engine ---
self.decision_engine = TradeDecisionEngine(self)
self.CustomLog("[PHASE3] Trade decision engine initialized with 40% probability threshold")
# --- Phase 4: Initialize trade execution engine ---
self.execution_engine = TradeExecutionEngine(self)
self.CustomLog("[PHASE4] Trade execution engine initialized")
# --- Scheduling (Phase 1.1) ---
# Schedule trade evaluation before market close
self.Schedule.On(self.DateRules.EveryDay(self.spx_index_symbol),
self.TimeRules.At(15, 47), # 3:47 PM ET
self.EvaluateSignal)
self.CustomLog("[PHASE3] Scheduled EvaluateSignal daily at 3:47 PM ET for trade evaluation")
# Schedule expiration check 2 minutes before market close
self.Schedule.On(self.DateRules.EveryDay(self.spx_index_symbol),
self.TimeRules.At(15, 58), # 3:58 PM ET
self.CheckExpiringPositions)
self.CustomLog("[PHASE5] Scheduled expiration check daily at 3:58 PM ET")
# --- Phase 2: Schedule IV calculation once per day ---
self.Schedule.On(self.DateRules.EveryDay(self.spx_index_symbol),
self.TimeRules.AfterMarketOpen(self.spx_index_symbol, 30),
self.CalculateDailyIV)
# --- Phase 5: Initialize trade management engine ---
self.trade_manager = TradeManagementEngine(self)
self.CustomLog("[PHASE5] Trade management engine initialized with 90% stop loss")
# self.CustomLog("[PHASE1] Algorithm initialization complete.")
def OnData(self, data: Slice):
"""
Phase 1: Basic option chain processing to verify ATM selection.
Phase 5: Monitor stop loss conditions.
"""
# Check if option chain data is available for trading
chain = data.OptionChains.get(self.option_symbol)
if chain is None or not chain:
return # Wait for chain data
# Only log ATM selection verification once per day around market open
if self.Time.hour == 9 and self.Time.minute == 31 and self.Time.second == 0:
# self.CustomLog(f"[PHASE1] Received SPX option chain for {self.Time.date()}.")
underlying_price = chain.Underlying.Price
if underlying_price <= 0:
# self.CustomLog(f"[PHASE1] Underlying price is not valid: {underlying_price}")
return
# Filter for contracts expiring tomorrow (1 DTE relative to trade entry)
target_expiry_date = self.Time.date() + timedelta(days=1)
contracts_1dte = [contract for contract in chain
if contract.Expiry.date() == target_expiry_date]
# Debug logging for DTE issue
# if not contracts_1dte:
# # self.CustomLog(f"[DEBUG-DTE] Target expiry date: {target_expiry_date} (Day: {target_expiry_date.strftime('%A')})")
# available_expiries = sorted(set(contract.Expiry.date() for contract in chain))
# if available_expiries:
# # self.CustomLog(f"[DEBUG-DTE] Available expiries ({len(available_expiries)}): {available_expiries[:5]} {'' if len(available_expiries) <= 5 else '...'}")
# # Log closest expiry to target to help diagnose
# closest_expiry = min(available_expiries, key=lambda d: abs((d - target_expiry_date).days))
# self.CustomLog(f"[DEBUG-DTE] Closest available expiry: {closest_expiry} ({(closest_expiry - self.Time.date()).days} DTE)")
# else:
# self.CustomLog(f"[DEBUG-DTE] No expiry dates available in chain")
if not contracts_1dte:
# self.CustomLog(f"[PHASE1] No contracts found for target expiry {target_expiry_date} in the chain.")
# self.CustomLog(f"[PHASE1] No 1 DTE contracts found for ATM selection check.")
return
else:
# self.CustomLog(f"[DEBUG-DTE] Got 1 DTE contract!")
contracts_to_use = contracts_1dte
# Find ATM strike (closest to underlying price)
atm_strike = min(contracts_to_use, key=lambda x: abs(x.Strike - underlying_price)).Strike
# self.CustomLog(f"[PHASE1] Underlying Price: {underlying_price:.2f}, Identified ATM Strike: {atm_strike}")
# Select ATM call and put
atm_call = next((c for c in contracts_to_use if c.Strike == atm_strike and c.Right == OptionRight.Call), None)
atm_put = next((c for c in contracts_to_use if c.Strike == atm_strike and c.Right == OptionRight.Put), None)
# if atm_call and atm_put:
# # self.CustomLog(f"[PHASE1] ATM Call found: {atm_call.Symbol.Value}")
# # self.CustomLog(f"[PHASE1] ATM Put found: {atm_put.Symbol.Value}")
# else:
# # self.CustomLog(f"[PHASE1] Could not find both ATM Call and Put for strike {atm_strike}.")
# Check IV calculation chain (30-day options)
iv_chain = data.OptionChains.get(self.iv_option_symbol)
if iv_chain is None or not iv_chain:
return
# # Log IV option chain data once per day (for verification)
# if self.Time.hour == 9 and self.Time.minute == 32 and self.Time.second == 0:
# # Get closest expiration to 30 days
# today = self.Time.date()
# target_date = today + timedelta(days=30)
# options_by_expiry = {}
# # Group options by expiry date
# for contract in iv_chain:
# expiry = contract.Expiry.date()
# if expiry not in options_by_expiry:
# options_by_expiry[expiry] = []
# options_by_expiry[expiry].append(contract)
# # Find expiry closest to 30 days
# if options_by_expiry:
# closest_expiry = min(options_by_expiry.keys(), key=lambda d: abs((d - today).days - 30))
# days_to_expiry = (closest_expiry - today).days
# # self.CustomLog(f"[PHASE2] 30-day IV chain: Found options expiring in {days_to_expiry} days ({closest_expiry})")
# # self.CustomLog(f"[PHASE2] Number of contracts in 30-day chain: {len(options_by_expiry[closest_expiry])}")
# else:
# # self.CustomLog("[PHASE2] No options found in the 30-day IV chain")
# Phase 5: Check stop loss if we have open positions
if self.trade_manager.check_stop_loss():
# Get current positions for tagging
open_positions = [pos for pos in self.Portfolio.Values if pos.Invested]
# Create liquidation orders
orders = [self.Liquidate(pos.Symbol, tag=f"STOP LOSS") for pos in open_positions]
def EvaluateSignal(self):
"""
Phase 2: Calculate indicators and log values
Phase 3: Evaluate trade signal using logistic regression model
Phase 4: Execute trades when conditions are met
"""
if self.IsWarmingUp:
return
# self.CustomLog(f"[PHASE3] EvaluateSignal triggered at {self.Time}")
# Verify we have option chain data with 1 DTE options
chain = self.OptionChain(self.option_symbol)
if not chain or not self.decision_engine.verify_dte(chain):
self.CustomLog("[PHASE3] Cannot evaluate signal: missing option chain or invalid DTE")
return
# Calculate and log the term structure slope
term_slope = self.CalculateTermSlope()
# Calculate and log the IV Z-Score
iv_zscore = self.CalculateIVZScore()
# Phase 3: Calculate probability and evaluate trade conditions
should_trade = self.ShouldEnterTrade()
# self.CustomLog(f"[PHASE3] Signal evaluation complete: Term Slope={term_slope:.6f}, IV Z-Score={iv_zscore:.6f}, Should Trade={should_trade}")
# Phase 4: If trade conditions are met, execute the trade
if should_trade:
self.CustomLog("[PHASE4] Trade conditions met, preparing to execute straddle position")
atm_options = self.GetATMOptions()
if atm_options.Call and atm_options.Put:
self.execution_engine.execute_straddle_position(atm_options)
else:
self.CustomLog("[PHASE4] Could not find valid ATM options for straddle execution")
def CalculateDailyIV(self):
"""
Calculate and store the daily ATM IV value using 30-day options.
Used for historical IV tracking.
"""
# if self.IsWarmingUp:
# # self.CustomLog(f"[PHASE2] Still in warm-up period, collecting historical IV data.")
# Only calculate once per day
if self.last_iv_date == self.Time.date():
return
current_iv = self.GetCurrentATMIV()
# Store the IV value if valid
if current_iv > 0:
self.historical_iv_values.append(current_iv)
self.last_iv_date = self.Time.date()
# self.CustomLog(f"[PHASE2] Daily IV calculated: {current_iv:.6f}, Historical IV count: {len(self.historical_iv_values)}")
else:
self.CustomLog(f"[PHASE2] Could not calculate valid IV for today.")
def yeo_johnson_transform(self, x, lambda_param=0.5):
"""
Apply Yeo-Johnson transformation to a single value.
Using fixed lambda=0.5 as per strategy specification.
"""
if x >= 0:
numerator = np.power((x + 1), lambda_param) - 1
return numerator / lambda_param if lambda_param != 0 else np.log(x + 1)
else:
numerator = np.power((-x + 1), (2 - lambda_param)) - 1
return -numerator / (2 - lambda_param) if lambda_param != 2 else -np.log(-x + 1)
def CalculateTermSlope(self):
"""
Calculate the term structure slope by:
1. Get SPX option chains for different expirations (1, 7, 14, 30, 60, 90 days)
2. For each expiration, find ATM options and calculate average IV
3. Perform linear regression on [days, IV] data points
4. Apply Yeo-Johnson transformation to the slope
"""
# Target DTE values for the term structure
target_dte_values = [1, 7, 14, 30, 60, 90]
term_structure_points = []
# Get SPX underlying price
spx_price = self.Securities[self.spx_index_symbol].Price
# Get full option chain for current time
option_chain = self.OptionChain(self.spx_index_symbol)
if not option_chain:
return 0
# Extract all available expiration dates from the option chain
today = self.Time.date()
available_expiries = []
expiry_options = {}
for contract in option_chain:
expiry = contract.Expiry.date()
days_to_expiry = (expiry - today).days
# Only consider expirations in the future
if days_to_expiry > 0:
if expiry not in expiry_options:
available_expiries.append((days_to_expiry, expiry))
expiry_options[expiry] = []
expiry_options[expiry].append(contract)
# Sort available expiries by DTE
available_expiries.sort(key=lambda x: x[0])
if not available_expiries:
return 0
# Find closest available expiration for each target DTE using a greedy approach
used_expiries = set()
dte_to_expiry = {}
for target_dte in target_dte_values:
# Find the closest available expiration that hasn't been used
closest_expiry = None
min_diff = float('inf')
for actual_dte, expiry in available_expiries:
if expiry in used_expiries:
continue
diff = abs(actual_dte - target_dte)
if diff < min_diff:
min_diff = diff
closest_expiry = (actual_dte, expiry)
# If found, mark as used and add to mapping
if closest_expiry:
actual_dte, expiry = closest_expiry
used_expiries.add(expiry)
dte_to_expiry[target_dte] = (actual_dte, expiry)
# Calculate term structure points using the mapped expiration dates
for target_dte, (actual_dte, expiry) in dte_to_expiry.items():
# Get the options for this expiration
options_for_expiry = expiry_options[expiry]
# Find the strike price closest to current SPX price
closest_strike = min(set(contract.Strike for contract in options_for_expiry),
key=lambda strike: abs(strike - spx_price))
# Get all options with that strike price
atm_options = [contract for contract in options_for_expiry
if contract.Strike == closest_strike]
# Average the IVs of these options
if atm_options:
total_iv = sum(option.ImpliedVolatility for option in atm_options)
avg_iv = total_iv / len(atm_options)
term_structure_points.append([actual_dte, avg_iv])
# Need at least 3 points for valid regression
if len(term_structure_points) < 3:
return 0
# Extract days and ivs from points
days = [point[0] for point in term_structure_points]
ivs = [point[1] for point in term_structure_points]
# Perform linear regression using numpy
slope_coefficient = np.polyfit(days, ivs, 1)[0]
# Apply Yeo-Johnson transformation to the slope
transformed_slope = self.yeo_johnson_transform(slope_coefficient)
self.CustomLog(f"[PHASE2] Term structure slope: raw={slope_coefficient:.6f}, transformed={transformed_slope:.6f}")
return transformed_slope
def CustomLog(self, message):
if not self.IsWarmingUp:
self.Log(f"{message}")
def GetCurrentATMIV(self):
"""
Get the current ATM implied volatility by:
1. Get the 30-day SPX option chain (from the specially filtered chain)
2. Find ATM options (within 0.5% of current price)
3. Calculate the average IV of ATM call and put
Modified to specifically use the 30-day option chain
"""
# Get current 30-day option chain
option_chain = self.OptionChain(self.iv_option_symbol)
# Check if the option chain exists and has contracts
if option_chain is None or not option_chain:
# self.CustomLog("[PHASE2] No options available in the 30-day chain")
return 0
# Get SPX price
spx_price = self.Securities[self.spx_index_symbol].Price
# Find options closest to 30 days to expiration
today = self.Time.date()
target_date = today + timedelta(days=30)
# Group options by expiry
options_by_expiry = {}
for contract in option_chain:
expiry = contract.Expiry.date()
if expiry not in options_by_expiry:
options_by_expiry[expiry] = []
options_by_expiry[expiry].append(contract)
# If no options available, return 0
if options_by_expiry is None or len(options_by_expiry) == 0:
# self.CustomLog("[PHASE2] No options grouped by expiry in the 30-day chain")
return 0
# Find expiry closest to 30 days
closest_expiry = min(options_by_expiry.keys(), key=lambda d: abs((d - today).days - 30))
thirty_day_options = options_by_expiry[closest_expiry]
days_to_expiry = (closest_expiry - today).days
# self.CustomLog(f"[PHASE2] Using options expiring in {days_to_expiry} days for IV calculation")
# Find the strike price closest to current SPX price
closest_strike = min(set(contract.Strike for contract in thirty_day_options),
key=lambda strike: abs(strike - spx_price))
# Get ATM options with the closest strike
atm_options = [contract for contract in thirty_day_options
if contract.Strike == closest_strike]
# Get ATM call and put for straddle
atm_call = next((contract for contract in atm_options
if contract.Right == OptionRight.Call), None)
atm_put = next((contract for contract in atm_options
if contract.Right == OptionRight.Put), None)
# If both call and put are found, return the average IV of the straddle
if atm_call is not None and atm_put is not None:
iv = (atm_call.ImpliedVolatility + atm_put.ImpliedVolatility) / 2
# self.CustomLog(f"[PHASE2] 30-day ATM straddle IV: {iv:.6f} (Call IV: {atm_call.ImpliedVolatility:.6f}, Put IV: {atm_put.ImpliedVolatility:.6f})")
return iv
# If only one is found, return that IV
elif atm_call is not None:
# self.CustomLog(f"[PHASE2] Using only 30-day ATM call IV: {atm_call.ImpliedVolatility:.6f}")
return atm_call.ImpliedVolatility
elif atm_put is not None:
# self.CustomLog(f"[PHASE2] Using only 30-day ATM put IV: {atm_put.ImpliedVolatility:.6f}")
return atm_put.ImpliedVolatility
# If no ATM options found, return 0
else:
# self.CustomLog("[PHASE2] No ATM options found for IV calculation")
return 0
def GetHistoricalATMStraddleIV(self, historical_date):
"""
Helper method that retrieves or estimates the historical ATM straddle IV
In a live implementation, this would use a database of historical IVs
For this implementation, we'll use our accumulated values from the warm-up period
"""
# For simplicity, we'll use our accumulated values during warm-up
# In a real implementation, additional steps would be needed to get
# historical option data for specific dates
# Not used in this implementation - we're accumulating data as we go in CalculateDailyIV
return 0
def CalculateIVZScore(self, lookback_period=252):
"""
Calculate the Z-Score of the current IV relative to historical IVs:
1. Get historical IV values (252 trading days)
2. Calculate mean and standard deviation
3. Get current IV
4. Calculate Z-Score as (current_iv - mean_iv) / std_iv
"""
# Check if we have enough historical data
if len(self.historical_iv_values) < lookback_period:
# self.CustomLog(f"[PHASE2] Insufficient historical IV data: {len(self.historical_iv_values)}/{lookback_period} days")
return 0
# Use the most recent lookback_period values
historical_iv_values = self.historical_iv_values[-lookback_period:]
# Handle missing data with linear interpolation
# Not needed here as we're ensuring valid values in CalculateDailyIV
# Calculate statistics
mean_iv = np.mean(historical_iv_values)
std_iv = np.std(historical_iv_values, ddof=1) # Use sample standard deviation
# Get current IV (average of ATM call and put - straddle IV)
current_iv = self.GetCurrentATMIV()
# Calculate Z-score with exact formula
z_score = (current_iv - mean_iv) / std_iv
# Log results with high precision
# self.CustomLog(f"[PHASE2] IV History: Mean={mean_iv:.6f}, StdDev={std_iv:.6f}")
# self.CustomLog(f"[PHASE2] Current IV: {current_iv:.6f}, Z-Score: {z_score:.6f}")
return z_score
def CalculateTradeProbability(self, term_slope, iv_zscore):
"""
Calculate the probability of a successful trade using the logistic regression model.
Parameters:
- term_slope: The term structure slope
- iv_zscore: The implied volatility Z-score
Returns:
- probability: Value between 0 and 1 representing trade probability
"""
# Use the model to calculate probability
z, probability = LogisticRegressionModel().calculate_probability(term_slope, iv_zscore)
# Log calculation details
# self.CustomLog(f"[PHASE3] Probability calculation: z={z:.6f}, probability={probability:.6f}")
return probability
def ShouldEnterTrade(self):
"""
Determine if a trade should be entered based on model and market conditions.
Returns:
- boolean: True if all trade conditions are met, False otherwise
"""
# Use decision engine to evaluate trade conditions
return self.decision_engine.should_enter_trade()
def GetATMOptions(self):
"""
Get ATM call and put options for straddle execution.
Returns object with Call and Put properties.
"""
# Get current option chain
chain = self.OptionChain(self.option_symbol)
if not chain:
self.CustomLog("[PHASE4] No option chain available for ATM selection")
return None
# Get SPX price
spx_price = self.Securities[self.spx_index_symbol].Price
# Get target expiration (1 DTE)
target_expiry = self.Time.date() + timedelta(days=1)
# Filter for options with target expiration
chain_for_expiry = [contract for contract in chain
if contract.Expiry.date() == target_expiry]
if not chain_for_expiry:
self.CustomLog("[PHASE4] No options found for target expiration")
return None
# Find strike price closest to current SPX price
closest_strike = min(set(contract.Strike for contract in chain_for_expiry),
key=lambda strike: abs(strike - spx_price))
# Get ATM call and put
atm_call = next((contract for contract in chain_for_expiry
if contract.Strike == closest_strike
and contract.Right == OptionRight.Call), None)
atm_put = next((contract for contract in chain_for_expiry
if contract.Strike == closest_strike
and contract.Right == OptionRight.Put), None)
if atm_call and atm_put:
self.CustomLog(f"[PHASE4] Selected ATM options: Strike={closest_strike}, "
f"Call={atm_call.Symbol}, Put={atm_put.Symbol}")
# Create result object
result = type('ATMOptions', (), {})()
result.Call = atm_call
result.Put = atm_put
return result
else:
self.CustomLog("[PHASE4] Could not find both ATM call and put options")
return None
def OnOrderEvent(self, orderEvent):
"""Handle order events for trade execution tracking."""
self.execution_engine.on_order_event(orderEvent)
# Phase 5: Initialize position tracking when both legs of straddle are filled
if orderEvent.Status == OrderStatus.Filled:
order_details = self.execution_engine.active_orders.get(orderEvent.OrderId)
if order_details:
# Check if both legs are filled
filled_orders = [o for o in self.execution_engine.active_orders.values()
if o.order.Status == OrderStatus.Filled]
if len(filled_orders) == 2: # Both call and put are filled
total_cost = sum(o.order.AbsoluteQuantity * o.order.AverageFillPrice
for o in filled_orders)
call_order = next(o for o in filled_orders if o.option_type == "Call")
put_order = next(o for o in filled_orders if o.option_type == "Put")
self.trade_manager.initialize_position_tracking(
call_order.order.Symbol,
put_order.order.Symbol,
total_cost
)
def CheckExpiringPositions(self):
"""Check for and liquidate positions expiring today."""
self.trade_manager.check_expiring_positions()
# --- Placeholder methods for future phases ---
def ExecuteStraddlePosition(self): # Phase 4
# Implementation needed
pass# region imports
from AlgorithmImports import *
# endregion
# region imports
import math
import numpy as np
# endregion
class LogisticRegressionModel:
"""
Implements the logistic regression model for SPX Long Straddle Strategy
with exact coefficient values as specified in the requirements.
"""
def __init__(self):
"""Initialize model with exact coefficient values."""
self.intercept = -0.60
self.term_slope_coefficient = -0.57
self.iv_zscore_coefficient = 0.36
def calculate_probability(self, term_slope, iv_zscore):
"""
Calculate trade probability using exact logistic regression formula.
Parameters:
- term_slope: The term structure slope
- iv_zscore: The implied volatility Z-score
Returns:
- probability: Value between 0 and 1 representing trade probability
"""
# Calculate logistic regression value with exact coefficients
z = self.intercept + (self.term_slope_coefficient * term_slope) + (self.iv_zscore_coefficient * iv_zscore)
# Apply sigmoid function with exact implementation
probability = 1 / (1 + math.exp(-z))
return z, probability # region imports
from datetime import datetime
from AlgorithmImports import *
# endregion
class TradeManagementEngine:
"""
Handles trade management functionality including stop loss monitoring
and order tagging for SPX Long Straddle Strategy.
"""
def __init__(self, algorithm):
"""Initialize with reference to main algorithm."""
self.algorithm = algorithm
self.initial_straddle_premium = None
self.stop_loss_level = None
def initialize_position_tracking(self, call_symbol, put_symbol, total_cost):
"""Initialize tracking for a new straddle position."""
self.initial_straddle_premium = total_cost
self.stop_loss_level = total_cost * 0.10 # 10% of initial (90% loss)
self.algorithm.CustomLog(f"[PHASE5] Stop loss set: Initial premium=${total_cost:.2f}, "
f"Stop level=${self.stop_loss_level:.2f} (10% of initial)")
def check_stop_loss(self):
"""Monitor position value and trigger stop loss if threshold is breached."""
if self.stop_loss_level is None or not self.algorithm.Portfolio.Invested:
return False
current_value = self.get_current_position_value()
if current_value <= 0: # Invalid value
return False
buffer = current_value - self.stop_loss_level
self.algorithm.CustomLog(f"[PHASE5] Stop loss check: Current value=${current_value:.2f}, "
f"Stop level=${self.stop_loss_level:.2f}, "
f"Remaining buffer=${buffer:.2f}")
if current_value <= self.stop_loss_level:
self.algorithm.CustomLog(f"[PHASE5] STOP LOSS TRIGGERED: "
f"Current value=${current_value:.2f} below stop level ${self.stop_loss_level:.2f}")
return True
return False
def get_current_position_value(self):
"""Calculate current value of the straddle position."""
if not self.algorithm.Portfolio.Invested:
return 0
total_value = 0
for holding in self.algorithm.Portfolio.Values:
if holding.Invested:
total_value += holding.HoldingsValue
return total_value
def tag_exit_orders(self, orders):
"""Tag exit orders with P&L information."""
if not self.initial_straddle_premium or not self.algorithm.Portfolio.Invested:
return
current_value = self.get_current_position_value()
pnl_pct = ((current_value / self.initial_straddle_premium) - 1) * 100
tag = f"{'WIN' if pnl_pct > 0 else 'LOSS'} {abs(pnl_pct):.2f}%"
for order in orders:
order.Tag = tag
position = self.algorithm.Portfolio[order.Symbol]
position_type = "Call" if order.Symbol.ID.OptionRight == OptionRight.Call else "Put"
self.algorithm.CustomLog(f"[PHASE5] Tagged {position_type} exit order {order.Id} with: {tag}")
def check_expiring_positions(self):
"""Check for and liquidate positions expiring today."""
if not self.algorithm.Portfolio.Invested:
return False
today = self.algorithm.Time.date()
expiring_positions = []
# Check each position in the portfolio for expiration
for holding in self.algorithm.Portfolio.Values:
if holding.Invested and holding.Symbol.ID.Date.date() == today:
expiring_positions.append(holding)
# Liquidate expiring positions
if expiring_positions:
self.algorithm.CustomLog(f"[PHASE5] Found {len(expiring_positions)} positions expiring today, liquidating...")
# Calculate P&L using portfolio values
total_cost_basis = sum(pos.HoldingsCost for pos in expiring_positions)
total_value = sum(pos.HoldingsValue for pos in expiring_positions)
if total_cost_basis != 0:
pnl_pct = ((total_value / abs(total_cost_basis)) - 1) * 100
tag = f"{'WIN' if pnl_pct > 0 else 'LOSS'} {abs(pnl_pct):.2f}%"
else:
tag = "EOD Exit"
# Liquidate with tag
for pos in expiring_positions:
position_type = "Call" if pos.Symbol.ID.OptionRight == OptionRight.Call else "Put"
self.algorithm.CustomLog(f"[PHASE5] Liquidating {position_type} position with tag: {tag}")
self.algorithm.Liquidate(pos.Symbol, tag)
return True
return False