| 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