| Overall Statistics |
|
Total Trades 1157 Average Win 0.93% Average Loss -0.68% Compounding Annual Return 6.551% Drawdown 18.300% Expectancy 0.175 Net Profit 84.685% Sharpe Ratio 0.585 Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.36 Alpha 0.049 Beta 0.148 Annual Standard Deviation 0.116 Annual Variance 0.014 Information Ratio -0.344 Tracking Error 0.169 Treynor Ratio 0.461 Total Fees $1670.52 |
class PriceEarningsMomentumAlgorithm(QCAlgorithm):
'''
A stock momentum strategy based on quarterly returns and earning growth.
Paper: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=299107
Online copy: https://www.trendrating.com/wp-content/uploads/dlm_uploads/2019/03/momentum.pdf
'''
def Initialize(self):
self.SetStartDate(2010, 1, 1) # Set Start Date
self.SetEndDate(2019, 9, 1) # Set End Date
self.SetCash(100000) # Set Strategy Cash
self.UniverseSettings.Resolution = Resolution.Daily # Resolution setting of universe selection
self.AddUniverse(self.CoarseSelection, self.FineSelection) # Coarse and Fine Universe Selection
self.nextRebalance = self.Time # Initialize next balance time
self.rebalanceDays = 90 # Rebalance quarterly
self.numOfCoarse = 100 # Number of coarse selected universe
self.longSymbols = [] # Symbol list of the equities we'd like to long
self.shortSymbols = [] # Symbol list of the equities we'd like to short
self.epsBySymbol = {} # Contains RollingWindow objects of EPS for every stock
def CoarseSelection(self, coarse):
'''
Pick the top 100 liquid equities as the coarse-selected universe
'''
# Before next rebalance time, just remain the current universe
if self.Time < self.nextRebalance:
return Universe.Unchanged
# Sort the equities (prices > 5) by Dollar Volume descendingly
selectedByDollarVolume = sorted([x for x in coarse if x.Price > 5 and x.HasFundamentalData],
key = lambda x: x.DollarVolume, reverse = True)
# Pick the top 100 liquid equities as the coarse-selected universe
return [x.Symbol for x in selectedByDollarVolume[:self.numOfCoarse]]
def FineSelection(self, fine):
'''
Select securities based on their quarterly return and their earnings growth
'''
symbols = [x.Symbol for x in fine]
# Get the quarterly returns for each symbol
history = self.History(symbols, self.rebalanceDays, Resolution.Daily)
history = history.drop_duplicates().close.unstack(level = 0)
rankByQuarterReturn = self.GetQuarterlyReturn(history)
# Get the earning growth for each symbol
rankByEarningGrowth = self.GetEarningGrowth(fine)
# Get the sum of rank for each symbol and pick the top ones to long and the bottom ones to short
rankSumBySymbol = {key: rankByQuarterReturn.get(key, 0) + rankByEarningGrowth.get(key, 0)
for key in set(rankByQuarterReturn) | set(rankByEarningGrowth)}
# Get 10 symbols to long and short respectively
sortedDict = sorted(rankSumBySymbol.items(), key = lambda x: x[1], reverse = True)
self.longSymbols = [x[0] for x in sortedDict[:10]]
self.shortSymbols = [x[0] for x in sortedDict[-10:]]
return [x for x in symbols if str(x) in self.longSymbols + self.shortSymbols]
def GetQuarterlyReturn(self, history):
'''
Get the rank of securities based on their quarterly return from historical close prices
Return: dictionary
'''
# Get quarterly returns for all symbols
# (The first row divided by the last row)
returns = history.iloc[0] / history.iloc[-1]
# Transform them to dictionary structure
returns = returns.to_dict()
# Get the rank of the returns (key: symbol; value: rank)
# (The symbol with the 1st quarterly return ranks the 1st, etc.)
ranked = sorted(returns, key = returns.get, reverse = True)
return {symbol: rank for rank, symbol in enumerate(ranked, 1)}
def GetEarningGrowth(self, fine):
'''
Get the rank of securities based on their EPS growth
Return: dictionary
'''
# Earning Growth by symbol
egBySymbol = {}
for stock in fine:
# Select the securities with EPS (> 0)
if stock.EarningReports.BasicEPS.ThreeMonths == 0:
continue
# Add the symbol in the dict if not exist
if not stock.Symbol in self.epsBySymbol:
self.epsBySymbol[stock.Symbol] = RollingWindow[float](2)
# Update the rolling window for each stock
self.epsBySymbol[stock.Symbol].Add(stock.EarningReports.BasicEPS.ThreeMonths)
# If the rolling window is ready
if self.epsBySymbol[stock.Symbol].IsReady:
rw = self.epsBySymbol[stock.Symbol]
# Caculate the Earning Growth
egBySymbol[stock.Symbol] = (rw[0] - rw[1]) / rw[1]
# Get the rank of the Earning Growth
ranked = sorted(egBySymbol, key = egBySymbol.get, reverse = True)
return {symbol: rank for rank, symbol in enumerate(ranked, 1)}
def OnData(self, data):
'''
Rebalance quarterly
'''
# Do nothing until next rebalance
if self.Time < self.nextRebalance:
return
# Liquidate the holdings if necessary
for holding in self.Portfolio.Values:
symbol = holding.Symbol
if holding.Invested and symbol.Value not in self.longSymbols + self.shortSymbols:
self.Liquidate(symbol, "Not Selected")
# Open positions for the symbols with equal weights
count = len(self.longSymbols + self.shortSymbols)
if count == 0:
return
# Enter long positions
for symbol in self.longSymbols:
self.SetHoldings(symbol, 1 / count)
# Enter short positions
for symbol in self.shortSymbols:
self.SetHoldings(symbol, -1 / count)
# Set next rebalance time
self.nextRebalance += timedelta(self.rebalanceDays)