| Overall Statistics |
|
Total Trades 3094 Average Win 0.64% Average Loss -0.62% Compounding Annual Return 5.280% Drawdown 33.700% Expectancy 0.062 Net Profit 63.778% Sharpe Ratio 0.332 Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.04 Alpha 0.016 Beta 0.469 Annual Standard Deviation 0.229 Annual Variance 0.053 Information Ratio -0.228 Tracking Error 0.232 Treynor Ratio 0.162 Total Fees $7885.85 |
class SeasonalitySignalAlgorithm(QCAlgorithm):
'''
A strategy that takes long and short positions based on historical same-calendar month returns
Paper: https://www.nber.org/papers/w20815.pdf
'''
def Initialize(self):
self.SetStartDate(2010, 1, 1) # Set Start Date
self.SetEndDate(2019, 8, 1) # Set End Date
self.SetCash(100000) # Set Strategy Cash
self.num_coarse = 100 # Number of equities for coarse selection
self.num_long = 5 # Number of equities to long
self.num_short = 5 # Number of equities to short
self.longSymbols = [] # Contain the equities we'd like to long
self.shortSymbols = [] # Contain the equities we'd like to short
self.UniverseSettings.Resolution = Resolution.Daily # Resolution of universe selection
self.AddUniverse(self.SameMonthReturnSelection) # Universe selection based on historical same-calendar month returns
self.nextRebalance = self.Time # Next rebalance time
def SameMonthReturnSelection(self, coarse):
'''
Universe selection based on historical same-calendar month returns
'''
# Before next rebalance time, just remain the current universe
if self.Time < self.nextRebalance:
return Universe.Unchanged
# Sort the equities with prices > 5 in DollarVolume decendingly
selected = sorted([x for x in coarse if x.Price > 5],
key=lambda x: x.DollarVolume, reverse=True)
# Get equities after coarse selection
symbols = [x.Symbol for x in selected[:self.num_coarse]]
# Get historical close data for coarse-selected symbols of the same calendar month
start = self.Time.replace(day = 1, year = self.Time.year-1)
end = Expiry.EndOfMonth(start) - timedelta(1)
history = self.History(symbols, start, end, Resolution.Daily).close.unstack(level=0)
# Get the same calendar month returns for the symbols
MonthlyReturn = {ticker: prices.iloc[-1]/prices.iloc[0] for ticker, prices in history.iteritems()}
# Sorted the values of monthly return
sortedReturn = sorted(MonthlyReturn.items(), key=lambda x:x[1], reverse=True)
# Get the symbols to long / short
self.longSymbols = [x[0] for x in sortedReturn[:self.num_long]]
self.shortSymbols = [x[0] for x in sortedReturn[-self.num_short:]]
# Note that self.longSymbols/self.shortSymbols contains strings instead of symbols
return [x for x in symbols if str(x) in self.longSymbols + self.shortSymbols]
def OnData(self, data):
'''
Rebalance every month based on same-calendar month returns effect
'''
# Before next rebalance, do nothing
if self.Time < self.nextRebalance:
return
count = len(self.longSymbols + self.shortSymbols)
# Open long positions
for symbol in self.longSymbols:
self.SetHoldings(symbol, 1/count)
# Open short positions
for symbol in self.shortSymbols:
self.SetHoldings(symbol, -1/count)
# Rebalance at the end of every month
self.nextRebalance = Expiry.EndOfMonth(self.Time) - timedelta(1)
def OnSecuritiesChanged(self, changes):
'''
Liquidate the stocks that are not in the universe
'''
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol, 'Removed from Universe')