Overall Statistics
Total Trades
76
Average Win
16.04%
Average Loss
-12.65%
Compounding Annual Return
2.974%
Drawdown
4.400%
Expectancy
0.105
Net Profit
6.042%
Sharpe Ratio
0.555
Probabilistic Sharpe Ratio
22.728%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.27
Alpha
0.001
Beta
0.073
Annual Standard Deviation
0.038
Annual Variance
0.001
Information Ratio
-1.18
Tracking Error
0.221
Treynor Ratio
0.292
Total Fees
$475.50
Estimated Strategy Capacity
$4000.00
Lowest Capacity Asset
QQQ 31TIRIE21U3XI|QQQ RIWIV7K5Z9LX
#region imports
from AlgorithmImports import *
#endregion
# Evaluate 10Delta Put Spread Selling Strategy 
# Using the following rules from OptionsAlpha.com
# 
# Becktest Duration: years
# Trade Frequency:  Weekly
# Capital: $100,000, 30% of capital per position
# Strategy:
# Entry Conditions
# Long Delta: 0.05
# Short Delta: 0.10
# Days to Expiration: 30
# Minimum IV Rank: 0
# Maximum IV Rank: 100
# Avoid Earnings: Yes
# Exit Conditions
# Profit Taking: None
# Stop Loss At: None
# Days to Expiration: Expire

# Reference:
# QC BullPutSpread Implimentation
# https://github.com/QuantConnect/Lean/blob/a8c81cad2a7807c8c868fcf578a6aa9c45769ec4/Common/Securities/Option/OptionStrategies.cs#L200
# QC Python Algo for Options Strategy
# https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/BasicTemplateOptionStrategyAlgorithm.py
# AlphaVantage Earnings Dates
# https://www.alphavantage.co/query?function=EARNINGS&symbol=IBM&apikey=demo

# Zhen Liu - Evolving decision making with humanity


from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from QuantConnect.Securities.Option import OptionPriceModels
from datetime import timedelta
import math
import numpy as np

class BullPutSpreadAlgorithm(QCAlgorithm):
    
    

    def Initialize(self):
        
        if self.LiveMode:
            self.Debug("Trading Live!")
      
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2022, 1, 1)
        
        self.SetWarmUp(30, Resolution.Daily) 
        
        self.SetCash(100000)

        UnderlyingSymbol =  "QQQ"
        equity = self.AddEquity(UnderlyingSymbol, dataNormalizationMode=DataNormalizationMode.Raw)
        self.symbol = equity.Symbol
        option = self.AddOption(self.symbol, Resolution.Minute) 

        equity.VolatilityModel = StandardDeviationOfReturnsVolatilityModel(30)
        
        # set our strike/expiry filter for this option chain
        option.SetFilter(lambda u: (u.Strikes(-200, +2)
                                     # Expiration method accepts TimeSpan objects or integer for days.
                                     # The following statements yield the same filtering criteria
                                     .Expiration(5, 45)))
                                     #.Expiration(TimeSpan.Zero, TimeSpan.FromDays(180))))

         
        #option = self.AddOption(symbol, Resolution.Minute)
        option.PriceModel = OptionPriceModels.CrankNicolsonFD()


        self.symbol = option.Symbol
        self.underlying = UnderlyingSymbol
        self.Num_Contracts = 0
        self.minimum_premium = 0.10
        self.position_size = 0.05

        # set strike/expiry filter for this option chain, weekly 
        #option.SetFilter(self.UniverseFunc)
        

        
        # use the underlying equity as the benchmark
        self.SetBenchmark(equity.Symbol)
        # self.Schedule.On(self.DateRules.EveryDay(symbol), self.TimeRules.BeforeMarketClose(symbol, 40), \
        # self.PositionManagement)         # time rule here tells it to fire 40 minutes before IWM's market close

        # Earnings Dates
        # self.EarningsDates = self.Get_EarningsDates()
        # self.next_earnings_date = datetime(2017, 1, 1)
        
        # def FineSelectionFunction(self, fine):
        #     fine = [x for x in fine if self.Time < x.EarningReports.FileDate + timedelta(days=30) 
        #             and x.EarningReports.FileDate != datetime.time()]
        #     return [i.Symbol for i in fine[:self.__numberOfSymbolsFine]]

    def UniverseFunc(self, universe):
        # include weekly contracts
        return universe.IncludeWeeklys().Expiration(TimeSpan.FromDays(5),
                                           TimeSpan.FromDays(45)).Strikes(-200,2)
    
    def OnData(self,slice):
        
        if (self.Time.hour,self.Time.minute) != (10, 30): return 
    
        
        # if self.Time < self.next_earnings_date:
        #     self.Debug('ED is ' + str(self.next_earnings_date))
        #     #return
        # else: 
        #     # self.Debug('ED is ' + str(self.next_earnings_date))
        #     self.next_earnings_date = self.NextEarningsDate() #self.next_earnings_date)
        #     self.Debug('New ED is ' + str(self.next_earnings_date))
        #     self.Debug('Today is ' + str(self.Time))
            
        

        
        # Set the stock symbol
        symbol = self.underlying  #"SPY"
        #self.Log(symbol)
        
        # self.next_earnings_date = self.NextEarningsDate() #self.EarningsDates)
        
        # Num_Contracts=0
        
        # Set the position size
        if slice.ContainsKey(symbol):
            # Trade stocks if invested.
            if self.Portfolio[symbol].Invested:
                self.Log('Current position has " + str(symbol) + " shares.') 
                #Num_Contracts = math.floor(self.Portfolio.TotalPortfolioValue/(100*slice[symbol].Price))
                #Num_Shares = 100 * Num_Contracts
                self.Liquidate(symbol)
                #self.MarketOrder(symbol, Num_Shares)     # Buy shares of underlying stocks
            
            option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
                #option_holding = [x for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
                
                
            if len(option_invested) == 1:
                self.Liquidate() #self.symbol)
            
            if len(option_invested) == 0:
                Capital_at_risk = self.position_size*self.Portfolio.TotalPortfolioValue
                #Num_Contracts = self.Portfolio[symbol].Quantity/100
                #if Num_Contracts > 0:
                self.TradeOptions(slice, Capital_at_risk) #, self.next_earnings_date )
    
    
                
    def NextEarningsDate(self):
        
        # Check earnings dates
        nearest = datetime.strptime('01/01/3000', "%m/%d/%Y")
        
        dates = pd.to_datetime(self.EarningsDates) #[str(self.equity.Symbol)]
    
        #self.Debug(dates) #self.EarningsDates.head())

        if len(dates) == 0: #self.EarningsDates[self.equity.Symbol]) == 0: 
            self.Debug('The ticker\'s earnings dates data are not available')
            return nearest
        else:
            nearest = dates[dates > self.Time].iloc[-1] 
            #for i in dates:
            #     date = pd.to_datetime(i) #datetime.strptime(str(i), "%m/%d/%Y") 
            #     if date > self.Time:
            #         nearest = date
            return nearest
    
    
            
    def TradeOptions(self, slice, capital): #, next_earnings_date):
        
        # self.Debug(next_earnings_date)
        
        contracts_selected = []
        
        for i in slice.OptionChains:
            if i.Key != self.symbol: continue
            chain = i.Value
            # filter the call options contracts
            put = [x for x in chain if x.Right == OptionRight.Put] 
            # sorted the contracts according to their expiration dates and choose the ATM options
            contracts = sorted(sorted(put, key = lambda x: abs(chain.Underlying.Price - x.Strike)), 
                                            key = lambda x: x.Expiry, reverse=False)
            if len(contracts) == 0: return #continue    
            #self.legs = [contracts[1], contracts[2]]
            
            for i in contracts:
                if (i.Greeks.Delta <= -0.01) and (i.Greeks.Delta >= -0.11):
                    contracts_selected.append(i)
            
            if len(contracts_selected) <= 1: 
                self.Debug("No contracts available.") 
                continue
            
            contracts = contracts_selected
            contract1 = contracts[0]
            contract2 = contracts[1]
            strike1 = contract1.Strike
            strike2 = contract2.Strike
            premium1 = contract1.BidPrice
            premium2 = contract2.AskPrice
            delta1 = contract1.Greeks.Delta
            delta2 = contract2.Greeks.Delta
            self.Log(delta1)
            # self.Log("Delta: " + str([i.Greeks.Delta for i in contracts]))
            
            if premium1-premium2 <= self.minimum_premium: return  # check the minimum premium $0.10
            exp = pd.to_datetime(contracts[0].Expiry)
            
            self.Debug('Exp is ' + str(exp))
            
            
            # self.Debug(type(next_earnings_date))
            # self.Debug(type(exp))
            
            # if exp >= next_earnings_date: 
            #     # self.Debug("Earnings date is before the expriation.")
            #     # self.Debug('Today is ' + str(self.Time))
            #     continue
            
            #return # does a better job in execution speed than continue or return
        
            unit_spread_risk =  abs(strike1 - strike2)*100
            Num_Contracts = math.floor(capital/unit_spread_risk)
            # short the put spreads
            self.Buy(OptionStrategies.BullPutSpread(self.symbol, strike1, strike2, exp ), Num_Contracts) 
   
               

    def OnOrderEvent(self, orderEvent):
        self.Log(str(orderEvent))
        
    def Get_EarningsDates(self):
        # import custom data
        # Note: dl must be 1, or it will not download automatically. 
        # Tutorial: Ensure you're downloading the direct file link - not the HTML page of 
        # the download. You can specify this by adding &dl=1 to the end of the Dropbox 
        # download URL.
        
        url = "https://www.dropbox.com/s/feaj2dcyrv9sozk/AMZN.csv?dl=1"
        data = self.Download(url).split('\r\n')
        df = pd.Series(data[1:]) #, header = 0)
        
        
        return df