| Overall Statistics |
|
Total Trades 2836 Average Win 0.07% Average Loss -0.04% Compounding Annual Return -18.561% Drawdown 21.000% Expectancy -0.356 Net Profit -18.559% Sharpe Ratio -2.414 Probabilistic Sharpe Ratio 0.006% Loss Rate 78% Win Rate 22% Profit-Loss Ratio 1.93 Alpha -0.184 Beta 0.128 Annual Standard Deviation 0.063 Annual Variance 0.004 Information Ratio -3.585 Tracking Error 0.111 Treynor Ratio -1.188 Total Fees $2874.30 |
import OptionsUniverse # Our Options Universe
import OptionsAlpha #Our options Alpha
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
from Execution.VolumeWeightedAveragePriceExecutionModel import VolumeWeightedAveragePriceExecutionModel
class Options (QCAlgorithm):
def Initialize(self):
self.SetStartDate(2019,1,4)
self.SetEndDate(2020,1,4)
self.SetCash(100000) # Set Strategy Cash
self.SetTimeZone(TimeZones.Chicago)
self.SetSecurityInitializer(lambda s: s.SetMarketPrice(self.GetLastKnownPrice(s)))
self.AddUniverseSelection(OptionsUniverse.universe()) #Calls Universe class, returns equities and underlying alphas
self.UniverseSettings.Resolution = Resolution.Minute #Minute resolution for options
self.UniverseSettings.DataNormalizationMode=DataNormalizationMode.Raw #how data goes into alg
self.UniverseSettings.FillForward = True #Fill in empty data will next price
self.UniverseSettings.ExtendedMarketHours = False #Does not takes in account after hours data
self.UniverseSettings.MinimumTimeInUniverse = 1 # each equity has to spend at least 1 hour in universe
self.UniverseSettings.Leverage=2 #Set's 2 times leverage
self.Settings.FreePortfolioValuePercentage = 0.5
self.AddAlpha(OptionsAlpha.alpha()) #Emits insights on equities and send automatic market orders on options
# we do not want to rebalance on insight changes
self.Settings.RebalancePortfolioOnInsightChanges = False;
# we want to rebalance only on security changes
self.Settings.RebalancePortfolioOnSecurityChanges = False;
#Set's equal weighting for all of our insights (equities) in our algorithm
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
self.SetRiskManagement(NullRiskManagementModel())
# Does not set any risk parameters
self.SetExecution(ImmediateExecutionModel())
def OnData(self, slice):
if self.IsWarmingUp: returnfrom QuantConnect.Data.UniverseSelection import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from QuantConnect.Data.Custom.SEC import *
from Selection.OptionUniverseSelectionModel import OptionUniverseSelectionModel
from datetime import timedelta, datetime
from math import ceil
from itertools import chain
import numpy as np
#imports necessary packages
class universe(FundamentalUniverseSelectionModel):
def __init__(self, filterFineData = True, universeSettings = None, securityInitializer = None):
super().__init__(filterFineData, universeSettings, securityInitializer)
# Number of stocks in Coarse Universe
self.NumberOfSymbolsCoarse = 2500
# Number of sorted stocks in the fine selection subset using the valuation ratio, EV to EBITDA (EV/EBITDA)
self.NumberOfSymbolsFine = 250
# Final number of stocks in security list, after sorted by the valuation ratio, Return on Assets (ROA)
self.NumberOfSymbolsInPortfolio =25
self.lastmonth = -1
self.dollarVolumeBySymbol = {}
def SelectCoarse(self, algorithm, coarse):
month= algorithm.Time.month
if month == self.lastmonth:
return Universe.Unchanged
self.lastmonth= month
#We only want to run universe selection once a month
# sort the stocks by dollar volume and take the top 2000
top = sorted([x for x in coarse if x.HasFundamentalData],
key=lambda x: x.DollarVolume, reverse=True)[:self.NumberOfSymbolsCoarse]
#assigns all the stocks from price to dollarVolumeBySymbol
self.dollarVolumeBySymbol = { i.Symbol: i.DollarVolume for i in top }
return list(self.dollarVolumeBySymbol.keys())
def SelectFine(self, algorithm, fine):
self.debttoequityMaxAllowance = 0.8
self.priceAllowance = 30
# QC500:
## The company's headquarter must in the U.S.
## The stock must be traded on either the NYSE or NASDAQ
## At least half a year since its initial public offering
## The stock's market cap must be greater than 500 million
## We want the stock's debt to equity ratio to be relatively low to enssure we are investing in stable companies
filteredFine = [x for x in fine if x.CompanyReference.CountryId == "USA"
and x.Price > self.priceAllowance
and (x.CompanyReference.PrimaryExchangeID == "NYS" or x.CompanyReference.PrimaryExchangeID == "NAS")
and (algorithm.Time - x.SecurityReference.IPODate).days > 180
and (x.EarningReports.BasicAverageShares.ThreeMonths * x.EarningReports.BasicEPS.TwelveMonths * x.ValuationRatios.PERatio > 5e8)
and 0 <= (x.OperationRatios.TotalDebtEquityRatioGrowth.OneYear) <= self.debttoequityMaxAllowance #this value will change in accordance to S&P Momentum
and x.FinancialStatements.BalanceSheet.AllowanceForDoubtfulAccountsReceivable.ThreeMonths <= 2.0 * x.FinancialStatements.CashFlowStatement.ProvisionandWriteOffofAssets.ThreeMonths
and (x.FinancialStatements.IncomeStatement.ProvisionForDoubtfulAccounts.TwoMonths <= 1.0*x.FinancialStatements.CashFlowStatement.ProvisionandWriteOffofAssets.ThreeMonths)
]
count = len(filteredFine)
if count == 0: return []
myDict = dict()
percent = self.NumberOfSymbolsFine / count
# select stocks with top dollar volume in every single sector based on specific sector ratios
# N=Normal (Manufacturing), M=Mining, U=Utility, T=Transportation, B=Bank, I=Insurance
for key in ["N", "M", "U", "T", "B", "I"]:
value1 = [x for x in filteredFine if x.CompanyReference.IndustryTemplateCode == key]
value2 = []
if key == "N":
value2 = [i for i in value1 if (1.0 <= i.OperationRatios.InventoryTurnover.ThreeMonths <= 2.0)]
if key == "M":
value2 = [i for i in value1 if i.OperationRatios.QuickRatio.ThreeMonths >= 1.0]
if key == "U":
value2 = [i for i in value1 if i.OperationRatios.InterestCoverage.ThreeMonths >= 2.0]
if key == "T":
value2 = [i for i in value1 if i.OperationRatios.ROA.ThreeMonths >= 0.04]
if key == "B":
value2 = [i for i in value1 if (i.FinancialStatements.IncomeStatement.OtherNonInterestExpense.ThreeMonths / i.FinancialStatements.IncomeStatement.TotalRevenue.ThreeMonths) < 0.60]
if key == "I":
value2 = [i for i in value1 if i.OperationRatios.LossRatio.ThreeMonths < 1.0]
if key != "N" or "M" or "U" or "T" or "B" or "I":
value2 = value1
value3 = sorted(value2, key=lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True)
myDict[key] = value3[:ceil(len(value3) * percent)]
# stocks in QC500 universe
topFine = chain.from_iterable(myDict.values())
# sort stocks in the security universe of QC500 based on Enterprise Value to EBITDA valuation ratio
sortedByEVToEBITDA = sorted(topFine, key=lambda x: x.ValuationRatios.EVToEBITDA , reverse=True)
# sort subset of stocks that have been sorted by Enterprise Value to EBITDA, based on the valuation ratio Return on Assets (ROA)
sortedByROA = sorted(sortedByEVToEBITDA[:self.NumberOfSymbolsFine], key=lambda x: x.ValuationRatios.ForwardROA, reverse=False)
# retrieve list of securites in portfolio
self.stocks = sortedByROA[:self.NumberOfSymbolsInPortfolio]
self.contract=[]
#creates an empty space in contract list for number of stocks we have
for x in self.stocks:
self.contract.append(None)
for x in self.stocks:
#sets the current symbol being looped through (will be used when calling other methods)
self.currentSymbol = x.Symbol
self.currentstock = x
#sets the index specific to each ticker in self.ticker. Indexes between ticker and options should always match
#need to write code that checks that option and ticker index is the same
self.currentIndex = self.stocks.index(x)
if self.contract[self.currentIndex] is None :
self.contract[self.currentIndex] = self.GetContract(algorithm)
#Generate a contract from the method GetContract and store in contract array which index corresponds to its ticker
#Following block of code combines both the symbols of equities and options
res = [i for i in self.contract if i]
self.result=[]
for t in res:
for x in t:
self.result.append(x)
self.newstocks= [x.Symbol for x in self.stocks]
#Returns our equities and hedged options
return [x for x in self.newstocks + self.result]
def GetContract(self, algorithm):
#set target strike 20% away
self.bound=0.2
lowertargetStrike = (self.currentstock.Price * (1-self.bound))
uppertargetStrike=(self.currentstock.Price * (1+self.bound))
#pulls contract data for select equity at current time
contracts=algorithm.OptionChainProvider.GetOptionContractList(self.currentSymbol, algorithm.Time)
#selects the type of option to be Put contract
puts = [x for x in contracts if x.ID.OptionRight == OptionRight.Put]
#sorts contracts by closet expiring date date and closest strike price (sorts in ascending order)
puts = sorted(sorted(puts, key = lambda x: x.ID.Date),
key = lambda x: x.ID.StrikePrice)
puts = [x for x in puts if x.ID.StrikePrice < lowertargetStrike]
#then selects all contracts that meet our expiration criteria
#We want between 30 and 60 days as we do not want to hold our options close to expiration
puts = [x for x in puts if 30<(x.ID.Date - algorithm.Time).days <= 60]
if not puts:
return
#selects the type of option to be Put contract
call = [x for x in contracts if x.ID.OptionRight ==OptionRight.Call]
#sorts contracts by closet expiring date date and closest strike price (sorts in ascending order)
call = sorted(sorted(call, key = lambda x: x.ID.Date),
key = lambda x: x.ID.StrikePrice)
call = [x for x in call if x.ID.StrikePrice > uppertargetStrike]
#then selects all contracts that meet our expiration criteria
call = [x for x in call if 30<(x.ID.Date - algorithm.Time).days <= 60]
if not call:
return
#will eventually return array of optimal puts and calls
return (puts[0],call[0])class alpha(AlphaModel):
def __init__(self,period = 63,resolution = Resolution.Daily):
self.period = period
self.resolution = resolution
self.insightPeriod = Time.Multiply(Extensions.ToTimeSpan(resolution), period)
self.symbolDataBySymbol ={}
self.optionDataBySymbol ={}
self.removed=[]
resolutionString = Extensions.GetEnumString(resolution, Resolution)
self.Name = '{}({},{})'.format(self.__class__.__name__, period, resolutionString)
self.day=None
def Update(self, algorithm, data):
insights=[]
if algorithm.Time.day == self.day:
return []
#Currently we only want to emit insights once a day
for symbol, symbolData in self.symbolDataBySymbol.items():
if not data.Bars.ContainsKey(symbol):
continue
#Pulls our symbol specific indicators
value = algorithm.Securities[symbol].Price
std=symbolData.STD
ema=symbolData.EMA
putcontract=None
callcontract=None
for contract ,info in self.optionDataBySymbol.items():
#Set each contract as either a put or call as we pull both
if info.Underlying.Value==symbol.Value and info.Right=="put":
putcontract=contract
if info.Underlying.Value==symbol.Value and info.Right=="call":
callcontract=contract
#The Alpha strategy longs if equity current price is a standard deviation below EMA and shorts if vice versa
#Emits flat otherwise
if value< (ema.Current.Value-std.Current.Value):
insights.append(Insight.Price(symbol, timedelta(days=1), InsightDirection.Up, 0.0025, 1.00,"Options", .5))
#If we are longing equity and have not already bought a put contract and one exists buy one
if putcontract is not None and not algorithm.Portfolio[putcontract].Invested:
algorithm.MarketOrder(putcontract,1,True)
#If we are trying to buy a put and we already have a call on the equity , we should sell it
if callcontract is not None and algorithm.Portfolio[callcontract].Invested:
algorithm.MarketOrder(callcontract,-1,True)
elif value> (ema.Current.Value+std.Current.Value):
insights.append(Insight.Price(symbol, timedelta(days=1), InsightDirection.Down,0.0025, 1.00,"Options", .5))
#If we are shorting equity and have not already bought a put contract and one exists buy one
if callcontract is not None and not algorithm.Portfolio[callcontract].Invested:
algorithm.MarketOrder(callcontract,1,True)
#If we are trying to buy a call and we already have a put on the equity , we should sell it
if putcontract is not None and algorithm.Portfolio[putcontract].Invested:
algorithm.MarketOrder(putcontract,-1,True)
else:
insights.append(Insight.Price(symbol, timedelta(days=1), InsightDirection.Flat,0.0025, 1.00,"ReversiontotheMean", .5))
if insights:
self.day = algorithm.Time.day
return insights
def OnSecuritiesChanged(self, algorithm, changes):
for y in changes.RemovedSecurities:
#There are two ways which we can remove options
#1 if an equity is no longer in our universe remove it and corresponding equity
if y.Symbol.SecurityType ==SecurityType.Equity:
self.removed.clear()
for contract ,info in self.optionDataBySymbol.items():
if info.Underlying==y.Symbol.Value:
self.removed.append(contract)
#pull all options with equity underlying it and add to self.removed
for x in self.removed :
optionData=self.optionDataBySymbol.pop(x,None)
#As we do not create any indicators with options we do not have any consolidators that we need to remove here
symbolData = self.symbolDataBySymbol.pop(y.Symbol, None)
if symbolData:
algorithm.SubscriptionManager.RemoveConsolidator(y.Symbol, symbolData.Consolidator)
#Remove our consolidators to not slow down our algorithm
#If the option is no longer the desired one and we also arent removing the equity remove it
elif y.Symbol.SecurityType ==SecurityType.Option:
if y.Underlying not in [x.Symbol for x in changes.RemovedSecurities]:
optionData=self.optionDataBySymbol.pop(y.Symbol,None)
#As we do not create any indicators with options we do not have any consolidators that we need to remove here
addedSymbols = [ x.Symbol for x in changes.AddedSecurities if (x.Symbol not in self.symbolDataBySymbol and x.Symbol.SecurityType ==SecurityType.Equity)]
if len(addedSymbols) == 0: return
#if no new symbols we do not need to generate any new instances
history = algorithm.History(addedSymbols, self.period, self.resolution)
#pulls history for all new symbols
for symbol in addedSymbols:
ema=ExponentialMovingAverage(self.period)
std=StandardDeviation(self.period)
consolidator = algorithm.ResolveConsolidator(symbol, self.resolution)
algorithm.RegisterIndicator(symbol, std, consolidator)
algorithm.RegisterIndicator(symbol, ema, consolidator)
#for each new symbol, generate an instance of the indicator std and ema
if not history.empty:
ticker = SymbolCache.GetTicker(symbol)
#if history isnt empty set the ticker as the symbol
for tuple in history.loc[ticker].itertuples():
ema.Update(tuple.Index, tuple.close)
std.Update(tuple.Index, tuple.close)
self.symbolDataBySymbol[symbol] = SymbolData(symbol, ema,std,consolidator)
#Records Symbol Data of each symbol including indicators and consolidator
options= [ x.Symbol for x in changes.AddedSecurities if (x.Symbol not in self.optionDataBySymbol and x.Symbol.SecurityType ==SecurityType.Option)]
if len(options) == 0: return
# Assign the option underlying
for option in options:
algorithm.Securities[option].Underlying = algorithm.Securities[option.Underlying]
newhistory = algorithm.History(options, self.period, Resolution.Minute)
if newhistory.empty: return
#if no new symbols we do not need to generate any new instances
for contract in options:
# for each new option, add to algorithm, save underlying and type of contract
underlying=contract.Underlying
if contract.ID.OptionRight ==OptionRight.Call:
right="call"
elif contract.ID.OptionRight ==OptionRight.Put:
right="put"
self.optionDataBySymbol[contract] = OptionData(contract,underlying,right)
#Records Option Data of each contract underlying symbol along with it being a put or call
class SymbolData:
def __init__(self, symbol, ema ,std,consolidator):
self.Symbol = symbol
self.EMA = ema
self.STD=std
self.Consolidator=consolidator
class OptionData:
def __init__(self,contract, underlying,right):
self.Contract=contract
self.Underlying=underlying
self.Right=right