| Overall Statistics |
|
Total Trades 1100 Average Win 0.74% Average Loss -0.58% Compounding Annual Return 3.377% Drawdown 16.300% Expectancy 0.107 Net Profit 37.865% Sharpe Ratio 0.36 Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.28 Alpha 0.033 Beta -0.017 Annual Standard Deviation 0.086 Annual Variance 0.007 Information Ratio -0.472 Tracking Error 0.162 Treynor Ratio -1.881 Total Fees $1456.27 |
class PriceEarningsAlgorithm(QCAlgorithm):
'''
A stock momentum strategy based on quarterly returns and earning growth modeled by AR(1).
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.Hour # Use hour resolution
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.coarseSymbols = [] # List of the coarse-selected symbols
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):
'''
Drop securities which have too low prices and select those with the highest dollar volume
'''
# 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
self.coarseSymbols = [x.Symbol for x in selectedByDollarVolume[:self.numOfCoarse]]
return self.coarseSymbols
def FineSelection(self, fine):
'''
Select securities based on their quarterly return and their scores
'''
# Get the quarterly returns for each symbol
history = self.History(self.coarseSymbols, 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 averaged rank for each symbol and pick the top ones to long and the bottom ones to short
avgRankBySymbol = {key: rankByQuarterReturn.get(key, 0) + rankByEarningGrowth.get(key, 0)
for key in set(rankByQuarterReturn) | set(rankByEarningGrowth)}
# Get symbols to long and short
sortedDict = sorted(avgRankBySymbol.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 self.coarseSymbols 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.)
rankBySymbol = {symbol: rank for rank, symbol in enumerate(sorted(returns, key = returns.get, reverse = True), 1)}
return rankBySymbol
def GetEarningGrowth(self, fine):
'''
Get the rank of securities based on their EPS growth
Return: dictionary
'''
# Select the securities with EPS
preSelected = [x for x in fine if x.EarningReports.BasicEPS.ThreeMonths > 0]
# Earning Growth by symbol
egBySymbol = {}
for stock in preSelected:
# 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 RW is ready
if self.epsBySymbol[stock.Symbol].IsReady:
rw = self.epsBySymbol[stock.Symbol]
egBySymbol[stock.Symbol] = (rw[0] - rw[1]) / rw[1]
# Get the rank of the Earning Growth
rankBySymbol = {symbol: rank for rank, symbol in enumerate(sorted(egBySymbol, key = egBySymbol.get, reverse = True), 1)}
return rankBySymbol
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)
# 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)