| Overall Statistics |
|
Total Orders 805 Average Win 3.34% Average Loss 532.07% Compounding Annual Return 127.256% Drawdown 9.500% Expectancy -0.455 Start Equity 2000000 End Equity 16162703.27 Net Profit 708.135% Sharpe Ratio 1.875 Sortino Ratio 2.654 Probabilistic Sharpe Ratio 97.051% Loss Rate 46% Win Rate 54% Profit-Loss Ratio 0.01 Alpha 0.885 Beta -0.474 Annual Standard Deviation 0.448 Annual Variance 0.201 Information Ratio 1.535 Tracking Error 0.487 Treynor Ratio -1.774 Total Fees $130.00 Estimated Strategy Capacity $110000.00 Lowest Capacity Asset SPXW 32MMIPI3G505Q|SPX 31 Portfolio Turnover 1.31% |
from AlgorithmImports import *
from datetime import datetime
import math
from scipy.stats import kurtosis
class MySecurityInitializer(BrokerageModelSecurityInitializer):
def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder):
super().__init__(brokerage_model, security_seeder)
def Initialize(self, security: Security):
# First call the base class initialization
super().Initialize(security)
class CombinedOptionsAlpha(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2022, 5, 16)
self.SetEndDate(2024, 12, 1)
self.SetCash(2000000)
self.SetTimeZone(TimeZones.NewYork)
self.straddle_alpha = DeltaHedgedStraddleAlpha(self)
self.condor_alpha = IronCondorAlpha(self)
# Set Brokerage Model
self.SetSecurityInitializer(MySecurityInitializer(
self.BrokerageModel,
FuncSecuritySeeder(self.GetLastKnownPrice)
))
# Set initial weights for each alpha (matching original 60-40 split)
self.alpha_weights = {
self.straddle_alpha: 1,
self.condor_alpha: 0,
}
# Track enabled status of strategies
self.strategy_enabled = {
self.straddle_alpha: True,
self.condor_alpha: False
}
self.profit_target = 1.5
self.stop_loss = 0.75
# Add these new tracking variables
self.last_performance_check = None
self.performance_window = 7
self.performance_threshold = -0.01
self.strategy_trades = {
self.straddle_alpha: [],
self.condor_alpha: []
}
# Initialize strategy performance tracking
self.strategy_performance = {
self.straddle_alpha: 0,
self.condor_alpha: 0
}
self.strategy_trade_counts = {
self.straddle_alpha: 0,
self.condor_alpha: 0
}
# Track trades and P&L for each strategy
self.strategy_trades = {
self.straddle_alpha: [],
self.condor_alpha: []
}
self.Log(f"[{self.Time}] Initialized CombinedOptionsAlpha with 2 strategies.")
def OnData(self, slice):
self.ManagePositions()
# self.CheckWeeklyPerformance()
# Pass option chain data to alpha models
if slice.OptionChains:
if self.strategy_enabled[self.condor_alpha]:
self.condor_alpha.OnOptionChainChanged(slice)
if self.strategy_enabled[self.straddle_alpha]:
self.straddle_alpha.OnOptionChainChanged(slice)
current_time = self.Time
# Straddle at 11:30
if (current_time.hour == 11 and
current_time.minute == 30 and
self.strategy_enabled[self.straddle_alpha]):
if self.straddle_alpha.ShouldTrade(slice):
self.Log("Executing Straddle Strategy")
self.ExecuteStrategyOrders(self.straddle_alpha, slice)
# Iron Condor between 15:00 and 15:55
if (current_time.hour == 15 and
0 <= current_time.minute <= 5):
# self.Log(f"Checking Iron Condor at {current_time}")
# self.Log(f"Strategy enabled: {self.strategy_enabled[self.condor_alpha]}")
if self.strategy_enabled[self.condor_alpha]:
# self.Log(f"Checking ShouldTrade for Iron Condor")
# self.Log(f"Portfolio Invested: {self.Portfolio.Invested}")
# self.Log(f"Kurtosis condition met: {self.condor_alpha.kurtosis_condition_met}")
if self.condor_alpha.ShouldTrade(slice):
# self.Log("Iron Condor ShouldTrade returned True")
trade_orders = self.condor_alpha.GenerateOrders(slice)
if trade_orders:
# self.Log(f"Generated Iron Condor orders: {trade_orders}")
weight = self.alpha_weights[self.condor_alpha]
weighted_orders = self.WeightOrders(trade_orders, weight)
self.ExecuteOrders(weighted_orders)
self.Log(f"Executed Iron Condor orders with {weight} weight")
else:
self.Log("No valid Iron Condor orders generated")
def OnOrderEvent(self, orderEvent):
"""Track trades and evaluate performance every 10 trades."""
if orderEvent.Status == OrderStatus.Filled:
# Determine which strategy the order belongs to
for strategy in [self.straddle_alpha, self.condor_alpha]:
if strategy.trade_open:
# Append trade details
self.strategy_trades[strategy].append({
'time': self.Time,
'pnl': orderEvent.FillPrice * orderEvent.FillQuantity
})
# Increment trade count
self.strategy_trade_counts[strategy] += 1
self.Log(f"{strategy.__class__.__name__} trade count: {self.strategy_trade_counts[strategy]}")
# Check performance after every 10 trades
if self.strategy_trade_counts[strategy] >= 8:
self.EvaluatePerformance(strategy)
# Reset trade count and trades after evaluation
self.strategy_trade_counts[strategy] = 0
self.strategy_trades[strategy] = []
def EvaluatePerformance(self, strategy):
"""Evaluate the performance of a strategy based on the last 10 trades."""
trades = self.strategy_trades.get(strategy, [])
if not trades:
self.Log(f"No trades for {strategy.__class__.__name__}. Skipping evaluation.")
return
# Calculate total P&L from the trades
total_pnl = sum(trade['pnl'] for trade in trades)
performance = total_pnl / self.Portfolio.TotalPortfolioValue
self.Log(f"Performance for {strategy.__class__.__name__}: {performance:.2%} over the last 10 trades")
# Disable or enable strategies based on performance
if performance < self.performance_threshold:
self.Log(f"Disabling {strategy.__class__.__name__} due to poor performance.")
self.DisableStrategy(strategy)
# Enable the alternate strategy
alternate_strategy = self.straddle_alpha if strategy == self.condor_alpha else self.condor_alpha
self.EnableStrategy(alternate_strategy)
else:
self.Log(f"{strategy.__class__.__name__} performance is acceptable. Strategy remains active.")
def CheckWeeklyPerformance(self):
if self.last_performance_check is None:
self.last_performance_check = self.Time
return
# Check if a week has passed
if (self.Time - self.last_performance_check) >= 15:
self.Log("Performing weekly performance evaluation")
# Calculate performance for each strategy
for strategy in [self.straddle_alpha, self.condor_alpha]:
trades = self.strategy_trades[strategy]
if trades:
total_pnl = sum(trade['pnl'] for trade in trades)
performance = total_pnl / self.Portfolio.TotalPortfolioValue
# Disable the strategy if performance is below threshold
if performance < self.performance_threshold:
self.Log(f"Disabling {strategy.__class__.__name__} due to poor performance: {performance:.2%}")
self.DisableStrategy(strategy)
# Enable the alternate strategy
alternate_strategy = self.straddle_alpha if strategy == self.condor_alpha else self.condor_alpha
self.EnableStrategy(alternate_strategy)
else:
# Enable the strategy if performance is acceptable
if not self.strategy_enabled[strategy]:
self.Log(f"Enabling {strategy.__class__.__name__}: {performance:.2%}")
self.EnableStrategy(strategy)
# Reset tracking
self.last_performance_check = self.Time
self.strategy_trades = {
self.straddle_alpha: [],
self.condor_alpha: []
}
def DisableStrategy(self, strategy):
"""Disable a strategy and liquidate its positions."""
self.strategy_enabled[strategy] = False
self.alpha_weights[strategy] = 0
if strategy.trade_open:
self.Liquidate()
strategy.trade_open = False
self.Log(f"Disabled {strategy.__class__.__name__}")
def EnableStrategy(self, strategy):
"""Enable a strategy and restore its weight."""
self.strategy_enabled[strategy] = True
self.alpha_weights[strategy] = 1 # Restore default weight (adjust as needed)
self.Log(f"Enabled {strategy.__class__.__name__}")
def ExecuteStrategyOrders(self, strategy, slice):
"""Execute orders for a specific strategy with weight applied"""
trade_orders = strategy.GenerateOrders(slice)
if trade_orders:
weight = self.alpha_weights[strategy]
weighted_orders = self.WeightOrders(trade_orders, weight)
self.ExecuteOrders(weighted_orders)
# self.Log(f"Executed {strategy.__class__.__name__} orders with {weight} weight")
def WeightOrders(self, orders, weight):
"""Apply strategy weight to order quantities"""
weighted_orders = []
for order in orders:
if len(order) == 2: # Iron Condor case: (strategy, quantity)
strategy, quantity = order
weighted_quantity = max(1, int(quantity * weight)) # Ensure minimum 1 contract
weighted_orders.append((strategy, weighted_quantity))
else: # Straddle case: (symbol, quantity, is_buy)
symbol, quantity, is_buy = order
weighted_quantity = max(1, int(quantity * weight)) # Ensure minimum 1 contract
weighted_orders.append((symbol, weighted_quantity, is_buy))
return weighted_orders
def ExecuteOrders(self, orders):
"""Execute the weighted orders"""
for order in orders:
try:
if len(order) == 2: # Iron Condor case
strategy, quantity = order
self.Buy(strategy, quantity)
self.Log(f"Executing Iron Condor order: {quantity} contracts")
else: # Straddle case
symbol, quantity, is_buy = order
if is_buy:
self.Buy(symbol, quantity)
self.Log(f"Buying {quantity} of {symbol}")
else:
self.Sell(symbol, quantity)
self.Log(f"Selling {quantity} of {symbol}")
except Exception as e:
self.Error(f"Order execution failed: {str(e)}")
def ManagePositions(self):
"""Centralized position management for both strategies"""
if not self.Portfolio.Invested:
return
total_pnl = sum([holding.UnrealizedProfit
for holding in self.Portfolio.Values
if holding.Invested])
# For each strategy, check if its positions need management
for alpha in [self.straddle_alpha, self.condor_alpha]:
if hasattr(alpha, 'trade_open') and alpha.trade_open:
if hasattr(alpha, 'initial_credit'): # Iron Condor case
if total_pnl >= alpha.initial_credit * self.profit_target:
self.Liquidate()
alpha.trade_open = False
self.Log(f"Closed position at profit target on {self.Time}")
elif total_pnl <= -alpha.max_potential_loss * self.stop_loss:
self.Liquidate()
alpha.trade_open = False
self.Log(f"Closed position at stop loss on {self.Time}")
elif hasattr(alpha, 'max_potential_loss'): # Straddle case
if total_pnl >= alpha.max_potential_loss * self.profit_target:
self.Liquidate()
alpha.trade_open = False
elif total_pnl <= -alpha.max_potential_loss * self.stop_loss:
self.Liquidate()
alpha.trade_open = False
class IronCondorAlpha:
def __init__(self, algorithm):
self.algorithm = algorithm # Store reference to main algorithm
self.Initialize()
def Initialize(self):
# Add SPX index
self.index = self.algorithm.AddIndex("SPX")
# Universe 1 (option1): Wide filter for kurtosis calculations
self.option1 = self.algorithm.AddIndexOption(self.index.Symbol, "SPXW")
self.option1.SetFilter(lambda universe: universe.IncludeWeeklys()
.Strikes(-30,30).Expiration(0, 0))
self._symbol1 = self.option1.Symbol
# Universe 2 (option2): Iron Condor filter for placing trades
self.option2 = self.algorithm.AddIndexOption(self.index.Symbol, "SPXW")
self.option2.SetFilter(lambda x: x.IncludeWeeklys().IronCondor(0, 20, 40))
self._symbol2 = self.option2.Symbol
# Risk and trade management parameters
self.max_portfolio_risk = 0.05
self.profit_target = 1.5
self.stop_loss = 0.75
self.trade_open = False
self.initial_credit = 0
self.max_potential_loss = 0
self.target_delta = 0.25
self.kurtosis_threshold = 2 # Changed to match original
self.current_date = None
self.kurtosis_condition_met = False
self.computed_kurtosis_today = False
def OnOptionChainChanged(self, slice):
# Check if a new day has started
if self.current_date != self.algorithm.Time.date():
self.current_date = self.algorithm.Time.date()
self.trade_open = False
self.kurtosis_condition_met = False
self.computed_kurtosis_today = False
self.algorithm.Log(f"New day reset for Iron Condor at {self.algorithm.Time}")
# Compute kurtosis at 9:31-9:36 AM
if (not self.computed_kurtosis_today and
self.algorithm.Time.hour == 9 and
self.algorithm.Time.minute >= 31 and
self.algorithm.Time.minute <= 36):
chain1 = slice.OptionChains.get(self._symbol1)
if chain1:
iv_values = [x.ImpliedVolatility for x in chain1
if x.ImpliedVolatility and 0 < x.ImpliedVolatility < 5]
if len(iv_values) > 10: # Using 10 as in original
daily_kurtosis = kurtosis(iv_values)
self.algorithm.Log(f"Iron Condor Kurtosis: {daily_kurtosis}")
if daily_kurtosis > self.kurtosis_threshold:
self.kurtosis_condition_met = True
self.algorithm.Log("Iron Condor Kurtosis condition met")
self.computed_kurtosis_today = True
def ShouldTrade(self, slice):
# Only check if we should trade based on conditions, not time
return (not self.algorithm.Portfolio.Invested and
self.kurtosis_condition_met)
def GenerateOrders(self, slice):
chain2 = slice.OptionChains.get(self._symbol2)
if not chain2:
return None
expiry = max([x.Expiry for x in chain2])
chain2 = sorted([x for x in chain2 if x.Expiry == expiry],
key=lambda x: x.Strike)
put_contracts = [x for x in chain2
if x.Right == OptionRight.PUT and
abs(x.Greeks.Delta) <= self.target_delta]
call_contracts = [x for x in chain2
if x.Right == OptionRight.CALL and
abs(x.Greeks.Delta) <= self.target_delta]
if len(call_contracts) < 2 or len(put_contracts) < 2:
return None
near_call = min(call_contracts,
key=lambda x: abs(x.Greeks.Delta - self.target_delta))
far_call = min([x for x in call_contracts if x.Strike > near_call.Strike],
key=lambda x: abs(x.Greeks.Delta - self.target_delta))
near_put = min(put_contracts,
key=lambda x: abs(x.Greeks.Delta + self.target_delta))
far_put = min([x for x in put_contracts if x.Strike < near_put.Strike],
key=lambda x: abs(x.Greeks.Delta + self.target_delta))
credit = (near_call.BidPrice - far_call.AskPrice) + (near_put.BidPrice - far_put.AskPrice)
spread_width = max(far_call.Strike - near_call.Strike,
near_put.Strike - far_put.Strike)
max_potential_loss = spread_width * 100 - credit * 100
total_portfolio_value = self.algorithm.Portfolio.TotalPortfolioValue
max_trade_risk = total_portfolio_value * self.max_portfolio_risk
contracts = int(max_trade_risk / max_potential_loss)
if contracts > 0:
iron_condor = OptionStrategies.IronCondor(
self._symbol2,
far_put.Strike,
near_put.Strike,
near_call.Strike,
far_call.Strike,
expiry
)
# Store trade parameters for position management
self.initial_credit = credit * 100 * contracts
self.max_potential_loss = max_potential_loss * contracts
self.trade_open = True
self.algorithm.Log(f"Generated iron condor at {self.algorithm.Time}, "
f"Contracts: {contracts}, Credit: ${self.initial_credit:.2f}")
return [(iron_condor, contracts)]
return None
class DeltaHedgedStraddleAlpha:
def __init__(self, algorithm):
self.algorithm = algorithm
self.Initialize()
def Initialize(self):
# Add SPX index
self.index = self.algorithm.AddIndex("SPX")
# Add SPY for Delta Hedging
self.spy = self.algorithm.AddEquity("SPY", Resolution.Minute)
self.spy.SetLeverage(1)
self.spy.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.spy_symbol = self.spy.Symbol
# Add SPX options
self.option = self.algorithm.AddIndexOption(self.index.Symbol, "SPXW")
self.option.SetFilter(lambda universe: universe.IncludeWeeklys()
.Strikes(-30, 30).Expiration(0, 0))
self.option_symbol = self.option.Symbol
# Risk and trade management parameters
self.max_portfolio_risk = 0.05
self.profit_target = 1.5
self.stop_loss = 0.75
self.trade_open = False
# Kurtosis calculation variables
self.kurtosis_threshold = 0
self.kurtosis_condition_met = False
self.computed_kurtosis_today = False
self.current_date = None
# Variables for delta hedging
self.hedge_order_ticket = None
self.net_delta = 0.0
self.max_potential_loss = 0.0
def OnOptionChainChanged(self, slice):
# Check if a new day has started
if self.current_date != self.algorithm.Time.date():
self.current_date = self.algorithm.Time.date()
self.trade_open = False
self.kurtosis_condition_met = False
self.computed_kurtosis_today = False
self.algorithm.Log(f"New day reset for Straddle at {self.algorithm.Time}")
# Liquidate any existing hedge at the start of a new day
if self.hedge_order_ticket and self.hedge_order_ticket.Status not in [OrderStatus.Filled, OrderStatus.Canceled]:
self.algorithm.CancelOrder(self.hedge_order_ticket.OrderId)
self.algorithm.Liquidate(self.spy_symbol)
self.algorithm.Liquidate(self.option_symbol)
# Compute kurtosis from option chain at 9:31-9:36 AM
if (not self.computed_kurtosis_today and
self.algorithm.Time.hour == 9 and
self.algorithm.Time.minute >= 31 and
self.algorithm.Time.minute <= 36):
chain = slice.OptionChains.get(self.option_symbol)
if chain:
iv_values = [x.ImpliedVolatility for x in chain
if x.ImpliedVolatility and 0 < x.ImpliedVolatility < 5]
if len(iv_values) > 3:
daily_kurtosis = kurtosis(iv_values)
if daily_kurtosis > self.kurtosis_threshold:
self.kurtosis_condition_met = True
self.algorithm.Log(f"Straddle Kurtosis met: {daily_kurtosis}")
self.computed_kurtosis_today = True
def ShouldTrade(self, slice):
return (not self.trade_open and
self.kurtosis_condition_met)
def GenerateOrders(self, slice):
chain = slice.OptionChains.get(self.option_symbol)
if not chain:
return None
# Find ATM strike
atm_strike = self.index.Price
closest_option = min(chain, key=lambda x: abs(x.Strike - atm_strike))
atm_strike = closest_option.Strike
# Filter for ATM call and put contracts with the highest Vega
atm_call_candidates = [x for x in chain
if x.Strike == atm_strike and
x.Right == OptionRight.CALL]
atm_put_candidates = [x for x in chain
if x.Strike == atm_strike and
x.Right == OptionRight.PUT]
if not atm_call_candidates or not atm_put_candidates:
return None
# Select contracts with highest Vega
atm_call = max(atm_call_candidates, key=lambda x: x.Greeks.Vega)
atm_put = max(atm_put_candidates, key=lambda x: x.Greeks.Vega)
# Calculate credit received from selling the straddle
credit = atm_call.BidPrice + atm_put.BidPrice
max_loss = abs(atm_call.Strike - self.index.Price) * 100 + credit * 100
if max_loss <= 0:
return None
# Position size calculation
total_portfolio_value = self.algorithm.Portfolio.TotalPortfolioValue
max_trade_risk = total_portfolio_value * self.max_portfolio_risk
contracts = int(max_trade_risk / max_loss)
if contracts <= 0:
return None
# Calculate delta hedge - Converting SPX delta to SPY (dividing by 10)
net_delta = (atm_call.Greeks.Delta + atm_put.Greeks.Delta) * contracts
required_spy_position = int(-net_delta * 10) # Multiply by 10 as SPX/SPY ratio is roughly 10:1
# Store trade parameters
self.trade_open = True
self.max_potential_loss = max_loss * contracts
self.net_delta = net_delta
# Log the hedge calculation
self.algorithm.Log(f"Delta calculation: Call Delta={atm_call.Greeks.Delta}, "
f"Put Delta={atm_put.Greeks.Delta}, "
f"Contracts={contracts}, "
f"Net Delta={net_delta}, "
f"Required SPY Position={required_spy_position}")
# Return orders as tuples: (symbol, quantity, is_buy)
orders = [
(atm_call.Symbol, contracts, False), # Sell call
(atm_put.Symbol, contracts, False), # Sell put
(self.spy_symbol, abs(required_spy_position), required_spy_position > 0) # Hedge
]
self.algorithm.Log(f"Generated straddle orders: Straddle Contracts={contracts}, "
f"Delta Hedge Size={abs(required_spy_position)}, "
f"Hedge Direction={'Long' if required_spy_position > 0 else 'Short'}")
return orders