Overall Statistics
Total Orders
73
Average Win
0.26%
Average Loss
-0.48%
Compounding Annual Return
-4.108%
Drawdown
5.500%
Expectancy
-0.049
Start Equity
2000000
End Equity
1996096.13
Net Profit
-0.195%
Sharpe Ratio
-1.882
Sortino Ratio
-1.622
Probabilistic Sharpe Ratio
15.498%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
0.54
Alpha
-0.084
Beta
-0.578
Annual Standard Deviation
0.191
Annual Variance
0.037
Information Ratio
-2.752
Tracking Error
0.304
Treynor Ratio
0.622
Total Fees
$1059.22
Estimated Strategy Capacity
$240000.00
Lowest Capacity Asset
QQQ XUERCY2UKAJQ|QQQ RIWIV7K5Z9LX
Portfolio Turnover
65.26%
from AlgorithmImports import *
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class OptimizedDeltaHedgingStrategy(QCAlgorithm):
    def Initialize(self):
        # Basic setup
        self.SetStartDate(2021, 12, 1)
        self.SetEndDate(2021, 12, 17)
        self.SetCash(2000000)
        
        # Add securities
        self.qqq = self.AddEquity("QQQ", Resolution.Hour)
        
        # Initialize strategy parameters first
        self.SetupStrategyParameters()
        
        # Add option with appropriate filter
        self.qqq_option = self.AddOption("QQQ", Resolution.Hour)
        self.qqq_option.SetFilter(self.OptionFilterFunc)
        
        # Initialize variables and set up schedules
        self.InitializeTrackingVariables()
        self.CalculateInitialHistoricalVolatility()
        
        # Set up schedules
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(9, 30), self.UpdateHistoricalVolatility)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 0), self.LogDailySummary)
        
        # Strategy rotation if testing multiple strategies
        if len(self.strategy_combinations) > 1:
            self.Debug("Testing multiple strategy combinations")
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(9, 31), self.RotateStrategy)
    
    def SetupStrategyParameters(self):
        """Set up strategy testing parameters"""
        # Create strategy testing combinations
        self.strategy_combinations = [
            # Base strategy - ATM calls
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "calls",
                "strike_selection": "atm",
                "iv_comparison": True
            },
            # ITM calls
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "calls",
                "strike_selection": "itm",
                "iv_comparison": True  
            },
            # OTM calls
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "calls",
                "strike_selection": "otm",
                "iv_comparison": True
            },
            # ATM puts
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "puts",
                "strike_selection": "atm",
                "iv_comparison": True
            },
            # ITM puts
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "puts",
                "strike_selection": "itm",
                "iv_comparison": True
            },
            # OTM puts
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 17),
                "option_type": "puts",
                "strike_selection": "otm",
                "iv_comparison": True
            },
            # Earlier expiry test - ATM calls
            {
                "strike_range": (-10, 10),
                "expiry": datetime(2021, 12, 10),
                "option_type": "calls",
                "strike_selection": "atm",
                "iv_comparison": True
            }
        ]
        
        # Set initial strategy
        self.current_strategy_index = 0
        self.current_strategy = self.strategy_combinations[0]
        self.strategy_performance = {}
        self.strategy_start_value = 0
    
    def InitializeTrackingVariables(self):
        """Initialize variables for tracking positions and performance"""
        # Option position tracking
        self.option_symbol = None
        self.option_strike = 0
        self.is_position_open = False
        self.position_type = None
        self.option_position_quantity = 0
        
        # Hedging parameters
        self.last_delta_hedge_time = datetime.min
        self.hedge_frequency = timedelta(hours=1)
        self.option_expiry = self.current_strategy["expiry"]
        self.start_trading_time = datetime(2021, 12, 1, 10, 0, 0)
        
        # Risk management parameters
        self.max_capital_usage = 0.05
        self.max_margin_usage = 0.70
        self.margin_buffer = 0.30
        
        # Volatility and PnL tracking
        self.historical_volatility = 0
        self.daily_pnl_start = 0
        self.today_date = None
    
    def OptionFilterFunc(self, universe):
        """Filter options based on strategy parameters"""
        strike_range = self.current_strategy["strike_range"]
        option_type = self.current_strategy["option_type"]
        expiry_range = self.option_expiry - self.Time
        
        # Verify expiry is valid
        if expiry_range.total_seconds() <= 0:
            return universe.Strikes(-5, 5).Expiration(timedelta(0), timedelta(20))
        
        # Apply filter based on option type
        if option_type == "calls":
            return universe.Strikes(strike_range[0], strike_range[1]).Expiration(timedelta(0), expiry_range).CallsOnly()
        elif option_type == "puts":
            return universe.Strikes(strike_range[0], strike_range[1]).Expiration(timedelta(0), expiry_range).PutsOnly()
        else:  # both
            return universe.Strikes(strike_range[0], strike_range[1]).Expiration(timedelta(0), expiry_range)
    
    def CalculateInitialHistoricalVolatility(self):
        """Calculate historical volatility at start"""
        hist_start = self.Time - timedelta(days=40)
        hist_end = self.Time - timedelta(days=1)
        history = self.History(self.qqq.Symbol, hist_start, hist_end, Resolution.Daily)
        
        if not history.empty and len(history) >= 25:
            closes = history['close'].values[-25:]
            log_returns = np.diff(np.log(closes))
            self.historical_volatility = np.std(log_returns) * np.sqrt(252)
    
    def UpdateHistoricalVolatility(self):
        """Daily update of historical volatility"""
        end_date = self.Time.date() - timedelta(days=1)
        start_date = end_date - timedelta(days=40)
        
        history = self.History(self.qqq.Symbol, start_date, end_date, Resolution.Daily)
        
        if len(history) >= 25:
            closes = history['close'].values[-25:]
            log_returns = np.diff(np.log(closes))
            self.historical_volatility = np.std(log_returns) * np.sqrt(252)
            
            self.daily_pnl_start = self.Portfolio.TotalPortfolioValue
            self.today_date = self.Time.date()
    
    def RotateStrategy(self):
        """Switch to the next strategy to test"""
        # Record performance of current strategy
        current_performance = self.Portfolio.TotalPortfolioValue - self.strategy_start_value
        strategy_key = self.FormatStrategyKey(self.current_strategy)
        self.strategy_performance[strategy_key] = current_performance
        
        self.Debug(f"Strategy {strategy_key} performance: ${current_performance:,.2f}")
        
        # Rotate to next strategy
        self.current_strategy_index = (self.current_strategy_index + 1) % len(self.strategy_combinations)
        self.current_strategy = self.strategy_combinations[self.current_strategy_index]
        
        # Reset for new strategy
        self.CloseAllPositions()
        self.InitializeTrackingVariables()
        self.strategy_start_value = self.Portfolio.TotalPortfolioValue
        
        self.Debug(f"Switching to strategy: {self.FormatStrategyKey(self.current_strategy)}")
    
    def FormatStrategyKey(self, strategy):
        """Create readable key for strategy"""
        return (f"{strategy['option_type']}_{strategy['strike_selection']}_"
                f"exp{strategy['expiry'].strftime('%m-%d')}")
    
    def SelectOption(self, chain):
        """Select appropriate option based on strategy"""
        underlying_price = chain.Underlying.Price
        strike_selection = self.current_strategy["strike_selection"]
        option_type = self.current_strategy["option_type"]
        
        # Filter by option type
        if option_type == "calls":
            contracts = [c for c in chain if c.Right == OptionRight.Call]
        elif option_type == "puts":
            contracts = [c for c in chain if c.Right == OptionRight.Put]
        else:
            # Select based on largest IV vs HV spread
            calls = [c for c in chain if c.Right == OptionRight.Call]
            puts = [c for c in chain if c.Right == OptionRight.Put]
            
            if not calls or not puts:
                return None
                
            # Find closest ATM options
            calls.sort(key=lambda x: abs(x.Strike - underlying_price))
            puts.sort(key=lambda x: abs(x.Strike - underlying_price))
            
            call_candidate = calls[0]
            put_candidate = puts[0]
            
            # Select best option type based on IV spread
            if abs(call_candidate.ImpliedVolatility - self.historical_volatility) > abs(put_candidate.ImpliedVolatility - self.historical_volatility):
                contracts = calls
            else:
                contracts = puts
        
        if not contracts:
            return None
            
        # Sort by strike price
        contracts.sort(key=lambda x: x.Strike)
        
        # Select appropriate strike based on strategy
        selected_contract = None
        
        if option_type == "calls" or (option_type == "both" and contracts[0].Right == OptionRight.Call):
            # Call selection logic
            if strike_selection == "atm":
                atm = [c for c in contracts if c.Strike >= underlying_price]
                selected_contract = atm[0] if atm else None
            elif strike_selection == "itm":
                itm = [c for c in contracts if c.Strike < underlying_price]
                selected_contract = itm[-1] if itm else None
            elif strike_selection == "otm":
                otm = [c for c in contracts if c.Strike > underlying_price]
                selected_contract = otm[1] if len(otm) > 1 else otm[0] if otm else None
        else:
            # Put selection logic
            if strike_selection == "atm":
                atm = [c for c in contracts if c.Strike <= underlying_price]
                selected_contract = atm[-1] if atm else None
            elif strike_selection == "itm":
                itm = [c for c in contracts if c.Strike > underlying_price]
                selected_contract = itm[0] if itm else None
            elif strike_selection == "otm":
                otm = [c for c in contracts if c.Strike < underlying_price]
                selected_contract = otm[-2] if len(otm) > 1 else otm[-1] if otm else None
        
        # Default to closest ATM if no match found
        if not selected_contract and contracts:
            selected_contract = min(contracts, key=lambda x: abs(x.Strike - underlying_price))
            
        return selected_contract
    
    def OpenOptionPosition(self, option_contract):
        """Open new option position based on strategy"""
        option_price = (option_contract.BidPrice + option_contract.AskPrice) / 2
        
        # Calculate safe position size
        underlying_price = self.Securities[self.qqq.Symbol].Price
        estimated_margin = (underlying_price * 0.2 + option_price) * 100
        
        available_capital = self.Portfolio.Cash * self.max_capital_usage
        max_by_capital = int(available_capital / option_price / 100) 
        max_by_margin = int(self.Portfolio.Cash * self.max_margin_usage / estimated_margin)
        
        self.option_position_quantity = max(1, min(max_by_capital, max_by_margin) // 2)
        self.option_symbol = option_contract.Symbol
        self.option_strike = option_contract.Strike
        
        # Determine position direction based on IV vs HV
        implied_vol = option_contract.ImpliedVolatility
        
        if implied_vol > self.historical_volatility:
            self.position_type = "short"
            self.Sell(self.option_symbol, self.option_position_quantity)
            self.Debug(f"SHORT {option_contract.Symbol}, Strike: {option_contract.Strike}, IV: {implied_vol:.2f}, HV: {self.historical_volatility:.2f}")
        else:
            self.position_type = "long"
            self.Buy(self.option_symbol, self.option_position_quantity)
            self.Debug(f"LONG {option_contract.Symbol}, Strike: {option_contract.Strike}, IV: {implied_vol:.2f}, HV: {self.historical_volatility:.2f}")
            
        self.is_position_open = True
    
    def PerformDeltaHedge(self, option_contract):
        """Execute delta hedging"""
        try:
            delta = option_contract.Greeks.Delta
            
            # Calculate required hedge quantity
            target_shares = -delta * self.option_position_quantity * 100
            current_shares = self.Portfolio[self.qqq.Symbol].Quantity
            shares_to_trade = int(target_shares - current_shares)
            
            # Ensure we don't exceed margin limits
            available_cash = self.Portfolio.Cash * self.margin_buffer
            safe_quantity = int(min(abs(shares_to_trade), available_cash / self.Securities[self.qqq.Symbol].Price))
            safe_shares = safe_quantity if shares_to_trade > 0 else -safe_quantity
            
            # Execute trade if needed
            if abs(safe_shares) > 0:
                if safe_shares > 0:
                    self.Buy(self.qqq.Symbol, abs(safe_shares))
                else:
                    self.Sell(self.qqq.Symbol, abs(safe_shares))
            
            self.last_delta_hedge_time = self.Time
            
        except Exception as e:
            self.Debug(f"Delta hedging error: {str(e)}")

    def CheckMarginStatus(self):
        """Monitor and manage margin usage"""
        margin_remaining = self.Portfolio.MarginRemaining
        margin_total = self.Portfolio.TotalMarginUsed + margin_remaining

        if margin_remaining < self.margin_buffer * margin_total:
            # Reduce position if margin gets low
            reduction_factor = 3

            if self.position_type == "long":
                qty_to_reduce = self.option_position_quantity // reduction_factor
                if qty_to_reduce > 0:
                    self.Sell(self.option_symbol, qty_to_reduce)
                    self.option_position_quantity -= qty_to_reduce
                    self.Debug(f"Margin protection: Reduced long position by {qty_to_reduce} contracts")
            else:  # short position
                qty_to_reduce = self.option_position_quantity // reduction_factor
                if qty_to_reduce > 0:
                    self.Buy(self.option_symbol, qty_to_reduce)
                    self.option_position_quantity -= qty_to_reduce
                    self.Debug(f"Margin protection: Reduced short position by {qty_to_reduce} contracts")

    def LogDailySummary(self):
        """Log daily performance metrics"""
        if self.today_date is None:
            return

        daily_pnl = self.Portfolio.TotalPortfolioValue - self.daily_pnl_start
        total_pnl = self.Portfolio.TotalProfit

        self.Debug(f"Date: {self.today_date} | Strategy: {self.FormatStrategyKey(self.current_strategy)} | " +
                  f"Daily PnL: ${daily_pnl:,.2f} | Total PnL: ${total_pnl:,.2f}")

        self.daily_pnl_start = self.Portfolio.TotalPortfolioValue

    def CloseAllPositions(self):
        """Close all open positions"""
        if self.is_position_open:
            if self.position_type == "long":
                self.Sell(self.option_symbol, self.option_position_quantity)
            else:
                self.Buy(self.option_symbol, self.option_position_quantity)

            # Close underlying hedge position
            underlying_position = self.Portfolio[self.qqq.Symbol].Quantity
            if underlying_position > 0:
                self.Sell(self.qqq.Symbol, underlying_position)
            elif underlying_position < 0:
                self.Buy(self.qqq.Symbol, abs(underlying_position))

            self.is_position_open = False
            self.Debug(f"Closed all positions for strategy: {self.FormatStrategyKey(self.current_strategy)}")

    def OnData(self, slice):
        """Main algorithm event handler"""
        if self.Time < self.start_trading_time:
            return

        if slice.OptionChains.Count == 0:
            return

        chain = list(slice.OptionChains.Values)[0]
        if not chain:
            return

        # Open new position if none exists
        if not self.is_position_open:
            selected_option = self.SelectOption(chain)
            if selected_option:
                self.OpenOptionPosition(selected_option)
            return

        # Perform delta hedging at scheduled intervals
        if (self.Time - self.last_delta_hedge_time) >= self.hedge_frequency:
            option_contract = next((contract for contract in chain 
                              if contract.Symbol == self.option_symbol), None)

            if option_contract is None:
                return

            self.CheckMarginStatus()
            self.PerformDeltaHedge(option_contract)

    def OnEndOfAlgorithm(self):
        """Final analysis at end of algorithm run"""
        # Record final strategy performance
        current_performance = self.Portfolio.TotalPortfolioValue - self.strategy_start_value
        strategy_key = self.FormatStrategyKey(self.current_strategy)
        self.strategy_performance[strategy_key] = current_performance

        # Find best performing strategy
        best_strategy = max(self.strategy_performance.items(), key=lambda x: x[1]) if self.strategy_performance else (None, 0)

        # Print summary of all strategies
        self.Debug("\n=== STRATEGY PERFORMANCE SUMMARY ===")
        for strat, pnl in sorted(self.strategy_performance.items(), key=lambda x: x[1], reverse=True):
            self.Debug(f"{strat}: ${pnl:,.2f}")

        # Print final results and analysis
        self.Debug("\n=== FINAL RESULTS ===")
        if best_strategy[0]:
            self.Debug(f"Best Strategy: {best_strategy[0]} with PnL: ${best_strategy[1]:,.2f}")

            # Parse which parameters were used in best strategy
            parts = best_strategy[0].split('_')
            option_type = parts[0]
            strike_selection = parts[1]
            expiry = parts[2].replace('exp', '')

            self.Debug(f"\nOptimal Parameters:")
            self.Debug(f"- Option Type: {option_type}")
            self.Debug(f"- Strike Selection: {strike_selection}")
            self.Debug(f"- Expiration: {expiry}")

            # Explain why this strategy performed best
            self.Debug("\nExplanation:")
            if strike_selection == "itm":
                self.Debug("ITM options have higher delta exposure, capturing more directional movement")
            elif strike_selection == "otm":
                self.Debug("OTM options have higher vega exposure, benefiting from volatility changes")
            else:
                self.Debug("ATM options have balanced delta/gamma exposure and good liquidity")

            if option_type == "calls":
                self.Debug("Calls were effective given the underlying price movement direction")
            else:
                self.Debug("Puts were effective due to downward price movement or volatility spikes")

            self.Debug("IV-HV comparison successfully identified options with mispriced volatility")