| Overall Statistics |
|
Total Trades 1568 Average Win 0.76% Average Loss -0.67% Compounding Annual Return 3.204% Drawdown 33.400% Expectancy 0.069 Net Profit 35.313% Sharpe Ratio 0.29 Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.14 Alpha 0.024 Beta 0.13 Annual Standard Deviation 0.143 Annual Variance 0.02 Information Ratio -0.475 Tracking Error 0.19 Treynor Ratio 0.318 Total Fees $2354.90 |
import numpy as np
class FamaFrenchFiveFactorsAlgorithm(QCAlgorithm):
''' Stocks Selecting Strategy based on Fama French 5 Factors Model
Reference: https://tevgeniou.github.io/EquityRiskFactors/bibliography/FiveFactor.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.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.num_coarse = 200 # Number of symbols selected at Coarse Selection
self.num_long = 5 # Number of stocks to long
self.num_short = 5 # Number of stocks to short
self.longSymbols = [] # Contains the stocks we'd like to long
self.shortSymbols = [] # Contains the stocks we'd like to short
self.nextLiquidate = self.Time # Initialize last trade time
self.rebalance_days = 30
# Set the weights of each factor
self.beta_m = 1
self.beta_s = 1
self.beta_h = 1
self.beta_r = 1
self.beta_c = 1
def CoarseSelectionFunction(self, coarse):
'''Drop securities which have no fundamental data or have too low prices.
Select those with highest by dollar volume'''
if self.Time < self.nextLiquidate:
return Universe.Unchanged
selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in selected[:self.num_coarse]]
def FineSelectionFunction(self, fine):
'''Select securities with highest score on Fama French 5 factors'''
# Select stocks with these 5 factors:
# MKT -- Book value per share: Value
# SMB -- TotalEquity: Size
# HML -- Operation profit margin: Quality
# RMW -- ROE: Profitability
# CMA -- TotalAssetsGrowth: Investment Pattern
filtered = [x for x in fine if x.ValuationRatios.BookValuePerShare
and x.FinancialStatements.BalanceSheet.TotalEquity
and x.OperationRatios.OperationMargin.Value
and x.OperationRatios.ROE
and x.OperationRatios.TotalAssetsGrowth]
# Sort by factors
sortedByMkt = sorted(filtered, key=lambda x: x.ValuationRatios.BookValuePerShare, reverse=True)
sortedBySmb = sorted(filtered, key=lambda x: x.FinancialStatements.BalanceSheet.TotalEquity.Value, reverse=True)
sortedByHml = sorted(filtered, key=lambda x: x.OperationRatios.OperationMargin.Value, reverse=True)
sortedByRmw = sorted(filtered, key=lambda x: x.OperationRatios.ROE.Value, reverse=True)
sortedByCma = sorted(filtered, key=lambda x: x.OperationRatios.TotalAssetsGrowth.Value, reverse=False)
stockBySymbol = {}
# Get the rank based on 5 factors for every stock
for index, stock in enumerate(sortedByMkt):
mktRank = self.beta_m * index
smbRank = self.beta_s * sortedBySmb.index(stock)
hmlRank = self.beta_h * sortedByHml.index(stock)
rmwRank = self.beta_r * sortedByRmw.index(stock)
cmaRank = self.beta_c * sortedByCma.index(stock)
avgRank = np.mean([mktRank,smbRank,hmlRank,rmwRank,cmaRank])
stockBySymbol[stock.Symbol] = avgRank
sorted_dict = sorted(stockBySymbol.items(), key = lambda x: x[1], reverse = True)
symbols = [x[0] for x in sorted_dict]
# Pick the stocks with the highest scores to long
self.longSymbols= symbols[:self.num_long]
# Pick the stocks with the lowest scores to short
self.shortSymbols = symbols[-self.num_short:]
return self.longSymbols + self.shortSymbols
def OnData(self, data):
'''Rebalance Every self.rebalance_days'''
# Liquidate stocks in the end of every month
if self.Time >= self.nextLiquidate:
for holding in self.Portfolio.Values:
# If the holding is in the long/short list for the next month, don't liquidate
if holding.Symbol in self.longSymbols or holding.Symbol in self.shortSymbols:
continue
# If the holding is not in the list, liquidate
if holding.Invested:
self.Liquidate(holding.Symbol)
count = len(self.longSymbols + self.shortSymbols)
# It means the long & short lists for the month have been cleared
if count == 0:
return
# Open long position at the start of every month
for symbol in self.longSymbols:
self.SetHoldings(symbol, 1/count)
# Open short position at the start of every month
for symbol in self.shortSymbols:
self.SetHoldings(symbol, -1/count)
# Set the Liquidate Date
self.nextLiquidate = self.Time + timedelta(self.rebalance_days)
# After opening positions, clear the long & short symbol lists until next universe selection
self.longSymbols.clear()
self.shortSymbols.clear()