Overall Statistics
Total Orders
378
Average Win
5.53%
Average Loss
-4.13%
Compounding Annual Return
20.156%
Drawdown
71.200%
Expectancy
0.564
Start Equity
100000
End Equity
2534719.68
Net Profit
2434.720%
Sharpe Ratio
0.537
Sortino Ratio
0.626
Probabilistic Sharpe Ratio
0.948%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.34
Alpha
0.054
Beta
1.992
Annual Standard Deviation
0.358
Annual Variance
0.128
Information Ratio
0.568
Tracking Error
0.217
Treynor Ratio
0.097
Total Fees
$1704.82
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY YUZ81Z0A99BA|SPY R735QTJ8XC9X
Portfolio Turnover
0.36%
Drawdown Recovery
1962
from AlgorithmImports import *

class QuarterlyLeapRotation(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2008, 1, 1)
        self.set_cash(100000)

        # Subscribe to daily resolution only
        self.spy = self.add_equity("SPY", Resolution.HOUR).symbol
        self.spy_option = self.add_option("SPY", Resolution.HOUR)
        self.spy_option.set_filter(self.option_filter)
        self.spy_option_symbol = self.spy_option.symbol
        
        self.set_warm_up(4, Resolution.DAILY)

        self.active_leaps = []  # Track open LEAPs

        self.action_buy_spy_for_all_cash = False
        self.action_buy_leap = False
        self.action_check_leap_exits = False

        # Schedule: Daily check if any LEAPs need to be sold (30 days before expiry)
        self.schedule.on(self.date_rules.every_day(self.spy),
                         self.time_rules.at(10, 0),
                         self.every_day_at_10)

        # Schedule: At quarter start, buy leaps.
        self.schedule.on(self.date_rules.month_start(1),
                    self.time_rules.at(11, 0),
                    self.every_month_start_11am)

        # Schedule: Buy spy with existing cash everyday.
        self.schedule.on(self.date_rules.every_day(self.spy),
                        self.time_rules.at(12, 0),
                        self.every_day_at_12)



    def every_day_at_10(self):
        self.action_check_leap_exits = True

    def every_month_start_11am(self):
        # Only buy at quarter start.
        if self.time.month in [1, 4, 7, 10]:
            self.action_buy_leap = True

    def every_day_at_12(self):
        self.action_buy_spy_for_all_cash = True 

    def option_filter(self, universe: OptionFilterUniverse):
        return universe.calls_only() \
                        .strikes(min_strike=10, max_strike=10) \
                       .expiration(timedelta(days=300), timedelta(days=370))

    

    def buy_spy_with_all_cash(self):
        spy_price = self.securities[self.spy].price
        if spy_price == 0:
            self.debug(f"[{self.time}] SPY price is not available")
            return

        quantity = int(self.portfolio.cash / self.securities[self.spy].price)
        if quantity > 0:
            self.market_order(self.spy, quantity, tag=f"converting cash to spy. cash={self.portfolio.cash}")
            self.debug(f"[{self.time}] SPY buy: {quantity} shares on {self.time.date()}")

    def check_leap_exits(self):
        today = self.time.date()
        remaining_leaps = []

        for leap in self.active_leaps:
            symbol = leap["symbol"]
            expiry = leap["expiry"]
            days_to_expiry = (expiry.date() - today).days

            if self.portfolio[symbol].invested and days_to_expiry <= 30:
                self.liquidate(symbol, tag="Exit leap")
                self.debug(f"[{self.time}] Sold LEAP {symbol} on {today}, {days_to_expiry} days to expiry")
            else:
                remaining_leaps.append(leap)

        self.active_leaps = remaining_leaps

    def buy_leap(self, data: Slice)-> bool:
        chain = data.option_chains.get(self.spy_option_symbol);
        if not chain:
            self.debug(f"[{self.time}] No spy option chain")
            return False

        calls = [x for x in chain if x.right == OptionRight.CALL]
        if not calls:
            self.debug(f"[{self.time}] No call options in chain")
            return False        
        
        # Filter out strikes which are not in 300 to 370 days. 
        calls = [x for x in chain if (x.expiry - self.time) < timedelta(days=370) and 
                (x.expiry - self.time) > timedelta(days=300)]
        if not calls:
            self.debug(f"[{self.time}] No call options with in 300-370 days to expiry")
            return False        

        # Pick ATM call with longest expiry
        sorted_calls = sorted(calls, key=lambda x: (x.expiry, abs(x.strike - self.securities[self.spy].price)))
        contract = sorted_calls[0]
        contract_symbol = contract.symbol
        option_price = contract.ask_price;

        if option_price == 0:
            self.debug("[{self.time}] Option price is 0.")
            return False

        # Use 10% of SPY holding value to buy LEAPs
        spy_qty = self.portfolio[self.spy].quantity
        spy_price = self.securities[self.spy].price
        if spy_price == 0:
            self.debug(f"[{self.time}] SPY price is zero")
            return False

        spy_value = spy_qty * spy_price
        budget = spy_value * 0.10
        quantity = int(budget // (option_price * 100))  # Each contract is 100 shares

        if quantity <= 0:
            self.debug("[{self.time}] Not enough SPY value to fund LEAP")
            return False

        # Sell just enough SPY to fund LEAP purchase
        total_cost = quantity * option_price * 100
        spy_to_sell = int(total_cost // spy_price) + 1

        if spy_qty < spy_to_sell:
            self.debug("[{self.time}] Not enough SPY to sell for LEAP")
            return False

        # Execute trades
        self.market_order(self.spy, -spy_to_sell, tag="Getting cash for leap")
        self.market_order(contract_symbol, quantity, tag="Buy leap")

        self.active_leaps.append({
            "symbol": contract_symbol,
            "expiry": contract.expiry
        })

        self.debug(f"[{self.time}] Bought {quantity}x LEAP {contract_symbol} on {self.time.date()}, expires {contract.expiry.date()}")
        return True

    def on_data(self, data: Slice):
        if self.is_warming_up:
            return 

        if self.action_buy_spy_for_all_cash:
            self.buy_spy_with_all_cash()
            self.action_buy_spy_for_all_cash = False 

        if self.action_check_leap_exits:
            self.check_leap_exits()
            self.action_check_leap_exits = False

        if self.action_buy_leap:
            if self.buy_leap(data):
                self.action_buy_leap = False