Overall Statistics
Total Orders
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
100000
End Equity
100000
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
-2.656
Tracking Error
0.127
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
from AlgorithmImports import *
from datetime import timedelta


class ChainedUniverseAlgorithm(QCAlgorithm):
    def initialize(self):
        # Setup
        self.set_start_date(2021, 2, 1)
        self.set_end_date(2021, 4, 1)
        self.set_cash(100_000)
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        self.set_benchmark("SPY")

        # Parameters
        self._min_price = 10

        # Universe
        self.universe_settings.asynchronous = True
        self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
        self.universe_settings.resolution = Resolution.DAILY
        # self.universe_settings.schedule.on = self.date_rules.every(DayOfWeek.Tuesday)
        self.set_security_initializer(CustomSecurityInitializer(self))
        universe = self.add_universe(self._fundamental_function)
        self.add_universe_options(universe, self._option_filter_function)
        
        # Initialize variables
        self._option_symbols = set()
        self._open_straddles = {}
        self.day = 0

        # Warm up so Greeks data are populated
        self.set_warm_up(5, Resolution.DAILY)

    def _fundamental_function(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if self.time.weekday() != 1 and self.time.weekday() != 2:
            return Universe.UNCHANGED

        filtered = (f for f in fundamental if f.price > self._min_price)
        sorted_by_dollar_volume = sorted(filtered, key=lambda f: f.dollar_volume, reverse=True)
        return [f.symbol for f in sorted_by_dollar_volume[:30]]

    def _option_filter_function(self, option_filter_universe: OptionFilterUniverse) -> OptionFilterUniverse:
        if self.time.weekday() != 1 and self.time.weekday() != 2:
            return
        # return option_filter_universe.straddle(60)
        return option_filter_universe.expiration(timedelta(20), timedelta(200)).strikes(-50, +50).delta(-0.65, 0.65).implied_volatility(0.3, 2)

    def on_data(self, data: Slice) -> None:
        if self.is_warming_up or self.day == self.time.day:
            return

        # Check if it is Tuesday
        if self.time.weekday() != 1 :
            return

        # Get chain and return if it is None
        chains = data.option_chains
        if not chains:
            return

        # Gather slopes for each symbol
        symbol_slopes = []  # list to store tuples (symbol, slope, call_contract, put_contract)
        
        # Trade
        iv_30 = None
        iv_180 = None
        for symbol, chain in chains.items():
            underlying_price = chain.underlying.price

            # Separate calls/puts for convenience
            calls = [x for x in chain if x.right == OptionRight.CALL and x.greeks is not None and x.greeks.delta > 0.35]
            puts  = [x for x in chain if x.right == OptionRight.PUT  and x.greeks is not None and x.greeks.delta < -0.35]
            
            if not calls or not puts:
                continue

            # Pick the call option closest to 30 days
            sorted_calls_30 = sorted(calls, key=lambda c: abs((c.expiry - self.time).days - 30))
            if not sorted_calls_30:
                continue
            expiry30_call = sorted_calls_30[-1].expiry
            calls_30day   = [c for c in calls if c.expiry == expiry30_call]
            if not calls_30day:
                continue
            contract_call = sorted(calls_30day, key=lambda c: abs(c.strike - underlying_price))[0]

            # Pick the put option closest to 30 days
            sorted_puts_30 = sorted(puts, key=lambda p: abs((p.expiry - self.time).days - 30))
            if not sorted_puts_30:
                continue
            expiry30_put = sorted_puts_30[-1].expiry
            puts_30day   = [p for p in puts if p.expiry == expiry30_put]
            if not puts_30day:
                continue
            contract_put = sorted(puts_30day, key=lambda p: abs(p.strike - underlying_price))[0]

            # For the 180-day calls
            sorted_calls_180 = sorted(calls, key=lambda c: abs((c.expiry - self.time).days - 180))
            if not sorted_calls_180:
                continue
            expiry180_call = sorted_calls_180[-1].expiry
            calls_180day   = [c for c in calls if c.expiry == expiry180_call]
            if not calls_180day:
                continue
            contract_call_180 = sorted(calls_180day, key=lambda c: abs(c.strike - underlying_price))[0]

            # For the 180-day puts
            sorted_puts_180 = sorted(puts, key=lambda p: abs((p.expiry - self.time).days - 180))
            if not sorted_puts_180:
                continue
            expiry180_put = sorted_puts_180[-1].expiry
            puts_180day   = [p for p in puts if p.expiry == expiry180_put]
            if not puts_180day:
                continue
            contract_put_180 = sorted(puts_180day, key=lambda p: abs(p.strike - underlying_price))[0]
            
            # Ensure we have IV data
            if (contract_call.implied_volatility is None or 
                contract_put.implied_volatility is None or
                contract_call_180.implied_volatility is None or
                contract_put_180.implied_volatility is None):
                continue
            
            # Calculate short-term IV (30 day) and long-term IV (180 day)
            iv_short = (contract_call.implied_volatility + contract_put.implied_volatility) / 2
            iv_long  = (contract_call_180.implied_volatility + contract_put_180.implied_volatility) / 2

            # Edge case: avoid division by zero if iv_long is 0 or None
            if iv_long <= 0:
                continue


        ############# HERE I REMOVED SOME CODE #############
            # Buy straddle
            self.market_order(contract_call.symbol, 1)
            self.market_order(contract_put.symbol, 1)

        # Update day so we don't trade multiple times
        self.day = self.time.day


class CustomSecurityInitializer(BrokerageModelSecurityInitializer):
    def __init__(self, algorithm: QCAlgorithm) -> None:
        super().__init__(algorithm.brokerage_model, FuncSecuritySeeder(algorithm.get_last_known_prices))
        self.algorithm = algorithm

    def initialize(self, security: Security) -> None:
        # First, call the superclass definition
        # This method sets the reality models of each security using the default reality models of the brokerage model
        super().initialize(security)

        # Overwrite the price model        
        if security.type == SecurityType.OPTION: # Option type
            security.price_model = OptionPriceModels.crank_nicolson_fd()