| 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