Overall Statistics
Total Trades
5
Average Win
30.75%
Average Loss
0%
Compounding Annual Return
70.519%
Drawdown
46.900%
Expectancy
0
Net Profit
69.200%
Sharpe Ratio
1.131
Sortino Ratio
1.41
Probabilistic Sharpe Ratio
46.183%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
0.032
Beta
4.77
Annual Standard Deviation
0.589
Annual Variance
0.347
Information Ratio
1.077
Tracking Error
0.495
Treynor Ratio
0.14
Total Fees
$5.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY YYFADQB3UR1I|SPY R735QTJ8XC9X
Portfolio Turnover
0.85%
from AlgorithmImports import *
import numpy as np
from math import log, sqrt, exp
from scipy.stats import norm
from QuantConnect.DataSource import *

class NOKLeapsBreakout(QCAlgorithm):

    def Initialize(self):
        self.Balance = 10000  
        self.SetStartDate(2023, 1, 1)
        self.SetEndDate(2024, 3, 20)
        self.SetCash(self.Balance)
        equity = self.AddEquity("SPY", Resolution.Minute)
        equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.equity = equity.Symbol
        self.SetBenchmark(self.equity)
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))
        option = self.AddOption("SPY", Resolution.Minute)
        self.option_symbol = option.Symbol
        self.Minimum_Expiry = 648
        self.Maximum_Expiry = 470
        self.Profit_Taking = 1.5
        self.number_options = 2
        self.Min_Option_Price = 0
        option.SetFilter(0, 5, timedelta(self.Minimum_Expiry), timedelta(99999))
        self.yieldCurveTwo = self.AddData(USTreasuryYieldCurveRate, "USTYCR").Symbol
        history = self.History(USTreasuryYieldCurveRate, self.yieldCurveTwo, 60, Resolution.Daily)
        self.SetWarmUp(timedelta(10))
        self.sigma = 0.165
        self.r = None
        self.LastTime = None
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 0), self.Plotting)
        self.mkt = []



    def OnData(self,data):
        if self.IsWarmingUp: return
        if data.ContainsKey(self.yieldCurveTwo):
            rates = data[self.yieldCurveTwo]
            self.r = ((rates.TwoYear) / 100)
            self.Log(f"| {self.Time} Rates found - {self.r}")
        option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        
        if option_invested:
            for i in range(len(option_invested)):

                if (self.Time + timedelta(self.Maximum_Expiry) > option_invested[i].ID.Date):
                    self.Liquidate(option_invested[i], "Too close to expiration")
                    self.Log (f"| {self.Time}   [+]---  Liquidate @ {str(self.Portfolio[option_invested[i]].Symbol.Value)} || Stock @ {str(self.Portfolio[self.equity].Price)}|| Profit/Loss @ {str(self.Portfolio[option_invested[i]].LastTradeProfit)}")
                    self.Log (f"| {self.Time}   [-]--- REASON: ||  <{(self.Maximum_Expiry)} DTE  | {(option_invested[i].ID.Date - self.Time).days} DTE")
                    
                if not self.LastTime == self.Time.day:
                    if len(option_invested)> ((self.number_options)-1):
                        self.Log("Currently Holding 2 Contracts - MAXIMUM") 
                        return
                    else: 
                        self.Log("only holding one contract")
                        for i in data.OptionChains:
                            chains = i.Value
                            self.BuyCall(chains)
          

        if not option_invested:
            self.Log("No Options invested")
            for i in data.OptionChains:
                chains = i.Value
                self.BuyCall(chains)
                             
        # Buy options - Call
    
    def BuyCall(self,chains):
        self.S = self.Securities[self.equity].Close
        
        expiry = sorted(chains,key = lambda x: x.Expiry, reverse=True)[0].Expiry
        calls = [i for i in chains if i.Expiry == expiry and i.Right == OptionRight.Call]
        call_contracts = sorted(calls,key = lambda x: abs(x.Strike - x.UnderlyingLastPrice < 0))
        
        self.Log(f'Contracts: {[contracts.Expiry for contracts in call_contracts]}')
        
        # check for wrong contracts
        wrong_contracts = [contracts for contracts in call_contracts if contracts.Expiry < self.Time + timedelta(self.Minimum_Expiry)] 
        if wrong_contracts:
            self.Debug("Contracts have Expiry less than minimum expiry required")

        if len(call_contracts) == 0: 
            self.Debug("No Contracts Found") 
            return

        self.Log("Contracts Found!")
        self.call = call_contracts[0]
        self.K = self.call.Strike
        self.T = ((self.call.Expiry - self.Time).days)/(365)
        option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        
        if (option_invested):
            for i in range(len(option_invested)): 
               if (str(self.Portfolio[option_invested[i]].Symbol.Value) == str(self.call)):
                self.Log("Already bought the Contract")
                return
        
        if self.r is None: 
            history = self.History(USTreasuryYieldCurveRate, self.yieldCurveTwo, 60, Resolution.Daily)
            twoyear = history['twoyear'][-1]
            self.r = twoyear / 100
            self.Debug((f"| Previous Rates Used -- Succesful {self.Time} -- {self.r}"))
        self.option_price = self.BlackScholesCall(self.S, self.K, self.T, self.r, self.sigma)
        self.Log((f"| Option Price Calculated - Succesful {self.Time} [+]--  Contracts  {str(self.call)} || Stock @ {str(self.call.UnderlyingLastPrice)} Option Price BS @ {(self.option_price)} rates: {self.r} Option Price Market @ {self.call.AskPrice} DTE @ {(self.call.Expiry - self.Time).days}"))
        
        if self.call.AskPrice > self.option_price:
            self.Log((f"| Option too Expensive! {self.Time})"))
            return
        
        if self.call.AskPrice == 0: return    
        self.SetHoldings(self.call.Symbol, 0.7)
        quantity = self.Portfolio[self.call.Symbol].Quantity   # <-- quantity is zero and the following orders will be invalid
        
        if quantity != 0:
            self.LimitOrder(self.call.Symbol, -quantity, (self.call.AskPrice * self.Profit_Taking))
            self.Log ("\r+-------------")
            self.Log (f"| {self.Time} [+] Bought Contracts -- {str(self.call)}")
            self.Log(f"Order Quantity filled: {self.Portfolio[self.call.Symbol].Quantity}; Fill price: {self.Portfolio[self.call.Symbol].AveragePrice} Limit price: {self.call.AskPrice * self.Profit_Taking}")
            self.LastTime = self.Time.day
            self.Log ("\r+-------------")
        else: self.Log(f"| {self.Time} -- No Money")    
        
        


    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        # Cancel remaining order if limit order or stop loss order is executed
        if order.Status == OrderStatus.Filled:
            if order.Type == OrderType.Limit or OrderType.StopMarket:
                self.Transactions.CancelOpenOrders(order.Symbol)
        if order.Status == OrderStatus.Filled and order.Type == OrderType.Limit:
            self.Log(f"{self.Time}   [+]---  SELL  {str(order.Quantity)} || Price @ {str(order.LimitPrice)}|| Contract @ {str(order.Symbol)}")
            if order.Status == OrderStatus.Canceled:
                self.Log(f"| {self.Time} -- Order Cancelled")
        # Liquidate before options are exercised
        if order.Type == OrderType.OptionExercise:
            self.Liquidate()
    
    def BlackScholesCall(self, S, K, T, r, sigma):
        d1 = (log(S / K) + (r + sigma ** 2 / 2) * T) / (sigma * sqrt(T))
        d2 = d1 - sigma * sqrt(T)
        return S * norm.cdf(d1) - K * exp(-r * T) * norm.cdf(d2)

    def Plotting(self):  
        hist = self.History(self.equity, 2, Resolution.Daily)['close'].unstack(level= 0).dropna() 
        self.mkt.append(hist[self.equity].iloc[-1])
        spy_perf = self.mkt[-1] / self.mkt[0] * self.Balance
        self.Plot('Strategy Equity', 'SPY', spy_perf)