Created with Highcharts 12.1.2EquityJun 10Jun 24Jul 8Jul 22Aug 5Aug 19Sep 2Sep 16Sep 30Oct 14Oct 28Nov 11990k995k1,000k1,005k-1-0.5000.20.4-0.200.20250k500k0200k400k
Overall Statistics
Total Orders
77
Average Win
0.21%
Average Loss
-0.25%
Compounding Annual Return
-1.275%
Drawdown
0.600%
Expectancy
-0.165
Start Equity
1000000
End Equity
994565
Net Profit
-0.544%
Sharpe Ratio
-17.16
Sortino Ratio
-7.403
Probabilistic Sharpe Ratio
0.000%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
0.85
Alpha
-0.064
Beta
-0
Annual Standard Deviation
0.004
Annual Variance
0
Information Ratio
-1.488
Tracking Error
0.112
Treynor Ratio
375.908
Total Fees
$68.00
Estimated Strategy Capacity
$260000.00
Lowest Capacity Asset
PG YMQUHZ6C29JA|PG R735QTJ8XC9X
Portfolio Turnover
0.46%
from AlgorithmImports import *
from datetime import timedelta
import numpy as np

from volatility_utils import VolatilityCalculator
from options_utils import OptionsManager
from trade_manager import TradeManager

"""
This strategy exploits earnings-related IV crush through ATM calendar 
spreads, entering positions when both IV term structure is abnormally 
steep and IV/RV ratio indicates overpricing before announcements. 

By selling front-month options against back-month options at identical 
strikes, it capitalizes on the asymmetric volatility compression between 
expirations, generating profits regardless of moderate price movement 
in the underlying. Primary risk includes gap moves exceeding expected 
range and abnormal post-earnings IV behavior.

Backtest Author  : u/shock_and_awful (reddit)
Strategy Creator : @VolatilityVibes (youtube)
"""

class EarningsVolatilityCalendarSpread(QCAlgorithm):
    def initialize(self):
        """
        Initialize the algorithm with settings, schedules, and variables.
        Sets up the earnings data source, universe settings, and scheduling for precise entry/exit timing.
        """
        self.set_start_date(2024, 6, 1)
        self.set_end_date(2024, 11, 3)
        self.set_cash(1000000)
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        # Initialize utility classes
        self.volatility = VolatilityCalculator(self)
        self.options = OptionsManager(self)
        self.trade_manager = TradeManager(self)

        # Local Enums for Earning report time. 
        # TODO: Will change later and replace with official LEAN ENUMS
        # Important because we dont know if LEAN expects 1 for BMO 
        self.IS_BMO = 0  # Before Market Open
        self.IS_AMC = 1  # After Market Close

        # Add earnings data source to track upcoming corporate earnings
        self.add_universe(EODHDUpcomingEarnings, self.selection)
        
        # Set universe settings for data resolution and normalization
        self.universe_settings.resolution = Resolution.MINUTE
        self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
        
        # Initialize variables to track earnings
        self.earnings_schedule = {}  # Will store: {symbol: (date, is_amc)}
        
        # Define threshold values from the technical specification
        self.threshold_volume = 1500000  # Minimum average daily volume requirement
        self.threshold_iv_rv_ratio = 1.25  # Minimum IV30/RV30 ratio requirement
        self.threshold_ts_slope = -0.00406  # Maximum term structure slope requirement
        self.risk_per_trade = 0.01  # 1% of portfolio per trade for position sizing
        
        # Define a symbol for scheduling (needed for market hours reference)
        self.schedule_symbol = Symbol.Create("SPY", SecurityType.EQUITY, Market.USA)
        self.add_equity("SPY", Resolution.MINUTE)
        
        # Schedule entry evaluation 15 minutes before market close
        self.schedule.on(
            self.date_rules.every_day(self.schedule_symbol), 
            self.time_rules.before_market_close(self.schedule_symbol, 15), 
            self.evaluate_entry_conditions
        )
        
        # Schedule position closing 15 minutes after market open
        self.schedule.on(
            self.date_rules.every_day(self.schedule_symbol), 
            self.time_rules.after_market_open(self.schedule_symbol, 15), 
            self.close_positions
        )
    
    def selection(self, earnings: List[EODHDUpcomingEarnings]) -> List[Symbol]:
        """
        Filter stocks with upcoming earnings within the next 4 days.
        
        Args:
            earnings: List of upcoming earnings announcements
            
        Returns:
            List of symbols that meet the selection criteria
        """
        # Look ahead 4 days to handle weekends properly
        selected_symbols = []
        # List of allowed symbols
        allowed_symbols = ["AAPL", "NVDA", "MSFT", "AMZN", "META", "GOOGL", "AVGO",\
                            "GOOG", "TSLA", "JPM", "LLY", "V", "XOM", "UNH", "MA", \
                            "NFLX", "COST", "JNJ", "PG", "WMT", "ABBV", "ADBE"]
        for earning in earnings:
            # Only process allowed symbols
            if earning.symbol.value not in allowed_symbols:
                continue
                
            if earning.report_date <= self.time + timedelta(days=4):
                # Store both the report date and whether it's AMC (1) or BMO (0)
                is_amc = earning.report_time == self.IS_AMC  
                self.earnings_schedule[earning.symbol] = (earning.report_date, is_amc)
                selected_symbols.append(earning.symbol)
        return selected_symbols
    

    def on_securities_changed(self, changes: SecurityChanges) -> None:
        """
        Handle changes to the securities universe.
        Adds options for new securities without liquidating positions for removed securities.
        
        Args:
            changes: Securities added or removed from the universe
        """
        # Add option chains for newly added securities
        for added in [security for security in changes.added_securities if security.type == SecurityType.EQUITY]:
            self.log(f"Added security {added.symbol} to universe")
            
            # Add option contracts for the equity
            option = self.add_option(added.symbol)
            
            # Set filter for option contracts - wider range to ensure finding suitable contracts
            option.set_filter(lambda u: (u.strikes(-2, +2).expiration(0, 60)))

    def evaluate_entry_conditions(self):
        """
        Evaluates entry conditions for calendar spreads 15 minutes before market close.
        This is the core strategy logic that checks all predictor variables against thresholds.
        """
        self.log(f"[3:45 PM] Evaluating entry conditions at {self.time}")
        
        # Track symbols that we've processed and can remove from earnings schedule
        symbols_to_remove = []
        
        # Check securities with earnings tomorrow
        for underlying_symbol, earnings_info in list(self.earnings_schedule.items()):
            earnings_date, is_amc = earnings_info
            
            # Only trade if earnings are tomorrow
            if earnings_date.date() != (self.time + timedelta(days=1)).date():
                continue
                
            self.log(f"[{underlying_symbol} Entry Check] - Earnings on {earnings_date} {'AMC' if is_amc else 'BMO'}")
            
            # Check if underlying is tradable and has valid price
            if not self.options.is_underlying_valid(underlying_symbol):
                # If we've decided not to enter, we can remove this from tracking
                symbols_to_remove.append(underlying_symbol)
                continue
            
            # Get option contracts for possible calendar spread
            atm_strike, expiry_dict = self.options.get_atm_options(underlying_symbol)
            if atm_strike is None or expiry_dict is None:
                symbols_to_remove.append(underlying_symbol)
                continue
                
            # Find suitable expiration pairs for calendar spread
            expiry_pair = self.options.find_expiry_pair(expiry_dict)
            if expiry_pair is None:
                symbols_to_remove.append(underlying_symbol)
                continue
                
            near_term_days, longer_term_days = expiry_pair

            # Check average volume (least expensive calculation first)
            avg_volume = self.volatility.calculate_average_volume(underlying_symbol)
            if avg_volume is None or avg_volume <= self.threshold_volume:
                symbols_to_remove.append(underlying_symbol)
                continue
        
            # Get term structure data
            days_array, iv_array = self.volatility.calculate_term_structure(expiry_dict)
            
            # Calculate term structure slope
            ts_slope_result = self.volatility.calculate_term_structure_slope(days_array, iv_array)
            if ts_slope_result is None or ts_slope_result[0] >= self.threshold_ts_slope:
                symbols_to_remove.append(underlying_symbol)
                continue

            ts_slope, term_structure = ts_slope_result
                            
            # Calculate realized volatility and IV/RV ratio using the already computed term structure
            iv_rv_ratio = self.volatility.calculate_iv_rv_ratio(underlying_symbol, term_structure)
            if iv_rv_ratio is None or iv_rv_ratio <= self.threshold_iv_rv_ratio:
                symbols_to_remove.append(underlying_symbol)
                continue
                
            # Log predictor variables
            self.log(
                f"....Term Slope: {round(ts_slope,5)}, "
                f"IV/RV: {round(iv_rv_ratio,2)}, "
                f"Avg Vol: {round(avg_volume,0)}"
            )

            # All conditions met, select contracts for calendar spread
            self.log(f"....All entry conditions met for {underlying_symbol}")
            
            # Select appropriate option contracts
            near_term_contract, longer_term_contract = self.options.select_option_contracts(
                expiry_dict, near_term_days, longer_term_days, OptionRight.CALL)
                
            if not near_term_contract or not longer_term_contract:
                symbols_to_remove.append(underlying_symbol)
                continue
            
            # Calculate position size
            position_size = self.options.calculate_position_size(
                near_term_contract, longer_term_contract, self.risk_per_trade)
            
            # Create calendar spread trade
            trade_success = self.trade_manager.enter_calendar_spread(
                underlying_symbol, near_term_contract, longer_term_contract, position_size)
            
            # If we didn't enter a trade, add to removal list
            if not trade_success:
                symbols_to_remove.append(underlying_symbol)
        
        # Clean up earnings schedule for symbols we've processed and decided not to trade
        for symbol in symbols_to_remove:
            if symbol in self.earnings_schedule:
                self.log(f"Removing {symbol} from earnings schedule - no trade entered")
                del self.earnings_schedule[symbol]

    def close_positions(self):
        """
        Close positions 15 minutes after market open for stocks that had earnings.
        Handles different timing for BMO (Before Market Open) and AMC (After Market Close) reports.
        """
        self.log(f"[9:45 AM] Checking for open positions to close at {self.time}")
        
        # Process exit logic based on earnings timing
        closed_symbols = self.trade_manager.process_earnings_exit(self.earnings_schedule)
        
        # Remove closed symbols from earnings schedule
        for symbol in closed_symbols:
            if symbol in self.earnings_schedule:
                del self.earnings_schedule[symbol]
                self.log(f"....Removed {symbol} from earnings schedule after position closed")
    
    def on_order_event(self, order_event):
        # Order event handling (if needed)
        pass 
from AlgorithmImports import *

class OptionsManager:
    """
    Handles all options-related functionality including finding ATM options,
    selecting expiration pairs, and building option positions.
    """
    
    def __init__(self, algorithm):
        """
        Initialize with a reference to the main algorithm.
        
        Args:
            algorithm: The main algorithm instance for accessing options chains and other methods
        """
        self.algorithm = algorithm
    
    def get_atm_options(self, symbol):
        """
        Get the ATM option contracts for the symbol.
        
        Args:
            symbol: The underlying security symbol
            
        Returns:
            tuple: (atm_strike, expiry_dict) or (None, None) if not found
        """
        # Get option chain
        option_chain = self.algorithm.option_chain(symbol)
        if option_chain is None or not list(option_chain):
            self.algorithm.log(f"....No option chain available for {symbol}")
            return None, None
                
        underlying_price = self.algorithm.securities[symbol].price
                
        # Find ATM strike - get the strike closest to current price
        strikes = [contract.strike for contract in option_chain]
        atm_strike = min(strikes, key=lambda x: abs(x - underlying_price))
        
        # Get all contracts at ATM strike
        atm_contracts = [contract for contract in option_chain if contract.strike == atm_strike]
        
        # Separate by expiration dates
        expiry_dict = {}
        for contract in atm_contracts:
            days_to_expiry = (contract.expiry - self.algorithm.time).days
            if days_to_expiry not in expiry_dict:
                expiry_dict[days_to_expiry] = []
            expiry_dict[days_to_expiry].append(contract)
        
        # Need at least two different expiration dates
        if len(expiry_dict) < 2:
            # self.algorithm.log(f"Not enough expiration dates for {symbol}")
            return None, None
            
        return atm_strike, expiry_dict
    
    def find_expiry_pair(self, expiry_dict):
        """
        Find suitable pair of expirations for a calendar spread.
        
        Args:
            expiry_dict: Dictionary of option contracts by days to expiry
            
        Returns:
            tuple: (near_term_days, longer_term_days) or None if not found
        """
        # Find near-term and longer-term expirations
        expiry_days = sorted(expiry_dict.keys())
        near_term_days = expiry_days[0]
        
        # Find expiration ~30 days later than nearest (as specified in technical doc)
        target_days = near_term_days + 30
        longer_term_days = None
        for d in expiry_days:
            if d > near_term_days:
                if longer_term_days is None or abs(d - target_days) < abs(longer_term_days - target_days):
                    longer_term_days = d
        
        if longer_term_days is None:
            # self.algorithm.log(f"No suitable longer-term expiration")
            return None
            
        return near_term_days, longer_term_days
    
    def select_option_contracts(self, expiry_dict, near_term_days, longer_term_days, right=OptionRight.CALL):
        """
        Select option contracts for the given expirations and option right.
        
        Args:
            expiry_dict: Dictionary of option contracts by days to expiry
            near_term_days: Days to expiry for near-term option
            longer_term_days: Days to expiry for longer-term option
            right: Option right (CALL or PUT)
            
        Returns:
            tuple: (near_term_contract, longer_term_contract) or (None, None) if not found
        """
        # Select ATM contracts for the two expirations with the specified right
        near_term_options = [c for c in expiry_dict[near_term_days] if c.right == right]
        longer_term_options = [c for c in expiry_dict[longer_term_days] if c.right == right]
        
        if not near_term_options or not longer_term_options:
            return None, None
            
        # Select the contracts
        near_term_contract = near_term_options[0]
        longer_term_contract = longer_term_options[0]
        
        return near_term_contract, longer_term_contract
    
    def calculate_position_size(self, near_term_contract, longer_term_contract, risk_per_trade):        
        """
        Calculate position size based on risk percentage of portfolio.
        Implements the fixed percentage position sizing from the technical specification.
        
        Args:
            near_term_contract: The near-term option contract
            longer_term_contract: The longer-term option contract
            risk_per_trade: Percentage of portfolio to risk per trade
            
        Returns:
            Number of contracts to trade
        """
        # Check if we should just return 1 contract
        if (self.algorithm.get_parameter("justOneContract", 0)) == 1:
            return 1

        # Get prices
        near_term_price = near_term_contract.bid_price
        longer_term_price = longer_term_contract.ask_price
        
        # Calculate net debit for the calendar spread
        net_debit = longer_term_price - near_term_price
        
        # Calculate maximum risk based on portfolio value
        max_risk = self.algorithm.portfolio.cash * risk_per_trade
        
        # Calculate position size
        if net_debit <= 0:
            # If net credit, use minimum size
            return 1
            
        position_size = max(1, int(max_risk / (net_debit * 100)))  # multiply by 100 for option contract multiplier
        
        return position_size
    
    def is_underlying_valid(self, symbol):
        """
        Check if the underlying security is valid and has a price.
        
        Args:
            symbol: The underlying security symbol
            
        Returns:
            bool: True if valid, False otherwise
        """
        if symbol not in self.algorithm.securities:
            self.algorithm.log(f"....{symbol} not in securities collection")
            return False
            
        underlying_price = self.algorithm.securities[symbol].price
        if underlying_price == 0:
            self.algorithm.log(f"....Invalid price for {symbol}")
            return False
            
        return True 
from AlgorithmImports import *
from datetime import timedelta

class TradeManager:
    """
    Handles trade execution, position tracking, and order management
    for the earnings volatility calendar spread strategy.
    """
    
    def __init__(self, algorithm):
        """
        Initialize with a reference to the main algorithm.
        
        Args:
            algorithm: The main algorithm instance for accessing trading methods
        """
        self.algorithm = algorithm
        self.trades = {}  # Will store: {symbol: (near_term_symbol, long_term_symbol, position_size)}
    
    def enter_calendar_spread(self, symbol, near_term_contract, longer_term_contract, position_size):
        """
        Enter a calendar spread trade for the given symbol.
        
        Args:
            symbol: The underlying security symbol
            near_term_contract: The near-term option contract
            longer_term_contract: The longer-term option contract
            position_size: Number of contracts to trade
            
        Returns:
            bool: True if the trade was entered successfully
        """
        if not near_term_contract or not longer_term_contract:
            self.algorithm.log(f"....Could not find suitable option contracts for {symbol}")
            return False
            
        # Add option contracts before trading them
        near_term_contract_symbol = self.algorithm.add_option_contract(near_term_contract).symbol
        longer_term_contract_symbol = self.algorithm.add_option_contract(longer_term_contract).symbol

        # Place orders for calendar spread
        # Sell near-term, buy longer-term (long calendar spread)
        near_term_order = self.algorithm.sell(near_term_contract_symbol, position_size)
        longer_term_order = self.algorithm.buy(longer_term_contract_symbol, position_size)
        
        # Store trade information for later reference and exit
        self.trades[symbol] = (near_term_contract.symbol, longer_term_contract.symbol, position_size)
        
        self.algorithm.log(f"....Entered calendar spread for {symbol}: ")
        self.algorithm.log(f"....Sold {position_size} of {near_term_contract.symbol}")
        self.algorithm.log(f"....Bought {position_size} of {longer_term_contract.symbol}")
        
        return True
    
    def close_positions_for_symbol(self, symbol):
        """
        Close positions for a specific symbol.
        
        Args:
            symbol: The underlying symbol to close positions for
            
        Returns:
            bool: True if positions were closed, False if no positions found
        """
        if symbol not in self.trades:
            return False
            
        near_term_symbol, longer_term_symbol, position_size = self.trades[symbol]
        
        # Close positions - buy back what we sold, sell what we bought
        self.algorithm.buy(near_term_symbol, position_size)
        self.algorithm.sell(longer_term_symbol, position_size)
        
        self.algorithm.log(f"....Closed position for {symbol} after earnings")
        
        # Remove from active trades
        del self.trades[symbol]
        
        return True
    
    def close_all_positions(self):
        """
        Close all open positions.
        
        Returns:
            int: Number of positions closed
        """
        positions_closed = 0
        for symbol in list(self.trades.keys()):
            if self.close_positions_for_symbol(symbol):
                positions_closed += 1
                
        return positions_closed
    
    def process_earnings_exit(self, earnings_schedule):
        """
        Process exit logic for positions based on earnings reports.
        
        Args:
            earnings_schedule: Dictionary mapping symbols to earnings info (date, is_amc)
            
        Returns:
            list: Symbols for which positions were closed
        """
        closed_symbols = []
        
        for underlying_symbol, trade_info in list(self.trades.items()):
            if underlying_symbol not in earnings_schedule:
                continue

            earnings_info = earnings_schedule.get(underlying_symbol)
            if earnings_info is None:
                continue
                
            earnings_date, is_amc = earnings_info
            should_exit = False
            
            # Check if we should exit based on earnings timing (BMO/AMC)
            if is_amc:
                # For AMC earnings, exit the morning after the earnings date
                if earnings_date.date() < self.algorithm.time.date():
                    should_exit = True
                    self.algorithm.log(f"....{underlying_symbol} Exited - AMC earnings on {earnings_date.date()} (previous day or earlier)")
            else:
                # For BMO earnings, exit the same morning
                if earnings_date.date() == self.algorithm.time.date():
                    should_exit = True
                    self.algorithm.log(f"....{underlying_symbol} Exited - BMO earnings today {earnings_date.date()}")
            
            if not should_exit:
                continue
                
            if self.close_positions_for_symbol(underlying_symbol):
                closed_symbols.append(underlying_symbol)
        
        return closed_symbols
    
    def get_active_trades(self):
        """
        Get list of active trades.
        
        Returns:
            dict: Dictionary of active trades
        """
        return self.trades 
import numpy as np
from scipy.interpolate import interp1d
from AlgorithmImports import *

class VolatilityCalculator:
    """
    Handles all volatility and term structure related calculations for the strategy.
    """
    
    def __init__(self, algorithm):
        """
        Initialize with a reference to the main algorithm.
        
        Args:
            algorithm: The main algorithm instance for accessing history and other methods
        """
        self.algorithm = algorithm
    
    def build_term_structure(self, days, ivs):
        """
        Build term structure using linear interpolation to estimate IV at any DTE.
        
        Args:
            days: Array of days to expiration
            ivs: Array of implied volatilities corresponding to each expiration
            
        Returns:
            Function that interpolates IV for any given days to expiration
        """
        # Sort by days to ensure proper ordering
        sort_idx = days.argsort()
        days_sorted = days[sort_idx]
        ivs_sorted = ivs[sort_idx]
        
        # Create interpolation function
        spline = interp1d(days_sorted, ivs_sorted, kind='linear', fill_value="extrapolate")
        
        def term_spline(dte):
            if dte < days_sorted[0]:
                return ivs_sorted[0]
            elif dte > days_sorted[-1]:
                return ivs_sorted[-1]
            else:
                return float(spline(dte))
                
        return term_spline
    
    def calculate_term_structure(self, expiry_dict):
        """
        Calculate term structure data from option expirations.
        
        Args:
            expiry_dict: Dictionary of option contracts by days to expiry
            
        Returns:
            tuple: (days_array, iv_array)
        """
        days_array = np.array(sorted(expiry_dict.keys()))
        
        # Calculate average IV for each expiration (combining calls and puts)
        iv_array = np.array([
            np.mean([c.implied_volatility for c in expiry_dict[days]])
            for days in days_array
        ])
        
        return days_array, iv_array
    
    def calculate_term_structure_slope(self, days_array, iv_array):
        """
        Calculate the slope of the IV term structure.
        
        Args:
            days_array: Array of days to expiration
            iv_array: Array of implied volatilities
            
        Returns:
            tuple: (slope_value, term_structure_function)
        """
        # Create interpolation function for IV term structure
        term_structure = self.build_term_structure(days_array, iv_array)
        
        # Get IV at near term and at 45 days
        near_term_days = days_array[0]
        near_term_iv = term_structure(near_term_days)
        iv45 = term_structure(45)
        
        # Calculate term structure slope
        ts_slope = (iv45 - near_term_iv) / (45 - near_term_days)
        
        return ts_slope, term_structure
    
    def calculate_realized_volatility(self, symbol):
        """
        Calculate realized volatility using Yang-Zhang method.
        This is a more accurate volatility measurement that accounts for overnight jumps.
        
        Args:
            symbol: Stock symbol to calculate volatility for
            
        Returns:
            Annualized realized volatility or None if calculation fails
        """
        # Get price history
        history = self.algorithm.history(symbol, 30, Resolution.DAILY)
        if history.empty or len(history) < 30:
            return None
            
        # Extract OHLC prices
        open_prices = history['open'].values
        high_prices = history['high'].values
        low_prices = history['low'].values
        close_prices = history['close'].values
        
        # Calculate components for Yang-Zhang volatility
        n = len(close_prices)
        close_to_close = np.log(close_prices[1:] / close_prices[:-1])
        open_to_open = np.log(open_prices[1:] / open_prices[:-1])
        log_hl = np.log(high_prices[1:] / low_prices[1:])
        
        # Calculate overnight volatility (close to open)
        overnight_vol = np.sum(np.square(np.log(open_prices[1:] / close_prices[:-1]))) / (n - 1)
        
        # Calculate open to close volatility
        open_close_vol = np.sum(np.square(np.log(close_prices[1:] / open_prices[1:]))) / (n - 1)
        
        # Calculate Rogers-Satchell volatility
        log_ho = np.log(high_prices[1:] / open_prices[1:])
        log_lo = np.log(low_prices[1:] / open_prices[1:])
        log_co = np.log(close_prices[1:] / open_prices[1:])
        rs_vol = np.sum(log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co)) / (n - 1)
        
        # Calculate k factor
        k = 0.34 / (1.34 + (n + 1) / (n - 1))
        
        # Combine the components for Yang-Zhang volatility
        yang_zhang_vol = overnight_vol + k * open_close_vol + (1 - k) * rs_vol
        
        # Convert to annualized volatility (assuming 252 trading days per year)
        annualized_vol = np.sqrt(yang_zhang_vol * 252)
        
        return annualized_vol
    
    def calculate_average_volume(self, symbol):
        """
        Calculate 30-day average trading volume for a symbol.
        Used as one of the three predictor variables.
        
        Args:
            symbol: Stock symbol to calculate average volume for
            
        Returns:
            30-day average volume or None if calculation fails
        """
        history = self.algorithm.history(symbol, 30, Resolution.DAILY)
        if history.empty or len(history) < 30:
            return None
            
        return history['volume'].mean()
    
    def calculate_iv_rv_ratio(self, symbol, term_structure):
        """
        Calculate the IV/RV ratio for a given symbol
        
        Args:
            symbol: The underlying security symbol
            term_structure: The precomputed term structure function
            
        Returns:
            float: IV/RV ratio or None if calculation failed
        """
        # Calculate 30-day realized volatility using Yang-Zhang method
        rv30 = self.calculate_realized_volatility(symbol)
        if rv30 is None or rv30 == 0:
            self.algorithm.log(f"....Could not calculate realized volatility for {symbol}")
            return None
        
        # Get implied volatility for 30 days from the provided term structure
        iv30 = term_structure(30)
        iv_rv_ratio = iv30 / rv30
        
        return iv_rv_ratio