| Overall Statistics |
|
Total Trades 38 Average Win 1.58% Average Loss -2.23% Compounding Annual Return -18.335% Drawdown 25.100% Expectancy -0.192 Net Profit -8.240% Sharpe Ratio -0.446 Probabilistic Sharpe Ratio 15.347% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 0.71 Alpha -0.145 Beta 0.082 Annual Standard Deviation 0.277 Annual Variance 0.077 Information Ratio -1.303 Tracking Error 0.301 Treynor Ratio -1.51 Total Fees $38.00 Estimated Strategy Capacity $23000000.00 Lowest Capacity Asset MARA VSI9G9W3OAXX |
# Your New Python File#based on https://www.quantconnect.com/tutorials/strategy-library/fama-french-five-factors
# deactivate stop lost - note somehow end with loss instead of profit in frame 1 so decide to off it
# change the balancing period from 30 to 31
# change the delta weight focus on book value and op margin
# no of long 4 short 3
# add benchmark Spy to see if it perform better than it
import numpy as np
from Risk.MaximumDrawdownPercentPerSecurity import MaximumDrawdownPercentPerSecurity
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):
# #frame 1
# self.SetStartDate(2016, 1, 1)
# self.SetEndDate(2020, 12, 31)
# #frame 2
# self.SetStartDate(2011, 1, 1)
# self.SetEndDate(2015, 12, 31)
# # #frame 3
# self.SetStartDate(2006, 1, 1)
# self.SetEndDate(2010, 12, 31)
# #frame 4
self.SetStartDate(2001, 1, 1)
self.SetEndDate(2005, 12, 31)
self.SetCash(100000) # Set Strategy Cash
#set stock selection - from 200 stocks refine to 20 stock split into 10 long and 10 short
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 = 4 # original is 5 Number of stocks to long
self.num_short = 3 # original is 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 = 31 #original is 30 but for consistency with factor make it 31
# Set the weights of each factor - see below original ratio all is 1 - correlation of each
self.beta_m = 2 #book value
self.beta_s = 1 #total equity
self.beta_h = 2 # op profit margin
self.beta_r = 1 # ROE total asset growth
self.beta_c = 1 # total asset growth
# # Risk model - additional parameter
# stopRisk = self.GetParameter("stopRisk")
# if stopRisk is None:
# stopRisk = 0.05 #5% stop loss
# self.SetRiskManagement(TrailingStopRiskManagementModel(float(stopRisk)))
#set SetBenchmark
self.SetBenchmark("SPY")
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], #share $ > 5 getting rid of penny stocks
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()# # 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.EmitInsights(
# # Creates an insight for our symbol, predicting that it will move up
# Insight.Price(symbol, timedelta(self.rebalance_days), InsightDirection.Up)
# )
# self.SetHoldings(symbol, 1/count)
# # Open short position at the start of every month
# for symbol in self.shortSymbols:
# self.EmitInsights(
# # Creates an insight for our symbol, predicting that it will move down
# Insight.Price(symbol, timedelta(self.rebalance_days), InsightDirection.Down)
# )
# self.SetHoldings(symbol, -1/count)#based on https://www.quantconnect.com/tutorials/strategy-library/fama-french-five-factors
#
# change the balancing period from 30 to 31
# change the delta weight focus on book value and op margin
# no of long 4 short 3
# add benchmark Spy to see if it perform better than it
# add normalization as raw
import numpy as np
from Risk.MaximumDrawdownPercentPerSecurity import MaximumDrawdownPercentPerSecurity
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):
#frame 1
# self.SetStartDate(2016, 1, 1)
# self.SetEndDate(2020, 12, 31)
# #frame 2
# self.SetStartDate(2011, 1, 1)
# self.SetEndDate(2015, 12, 31)
# # #frame 3
# self.SetStartDate(2006, 1, 1)
# self.SetEndDate(2010, 12, 31)
# #frame 4
# self.SetStartDate(2001, 1, 1)
# self.SetEndDate(2005, 12, 31)
#11 year test
# self.SetStartDate(2010, 1, 1)
# self.SetEndDate(2020, 12, 31)
# self.SetCash(100000) # Set Strategy Cash
# MPT high-low
self.SetStartDate(2021, 1, 1)
self.SetEndDate(2021, 6, 4)
# self.SetCash(2060) # Set Strategy Cash high low
self.SetCash(1410) # Set Strategy Cash high
#set stock selection - from 200 stocks refine to 20 stock split into 10 long and 10 short
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
#set data normalization
self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.TotalReturn))
#note without data normalization - it will be factoring splits and dividends
# DataNormalizationMode.Adjusted //Factoring in splits and dividends, default setting if no normalization
# .SplitAdjusted // Just factoring splits, paying dividends as cash
# .TotalReturn //Adding dividends to asset price
# .Raw // Price as raw, dividends paid as cash, quantity adjusted on splits
self.num_coarse = 200 # Number of symbols selected at Coarse Selection
self.num_long = 4 # original is 5 Number of stocks to long 4
self.num_short = 2 # original is 5 Number of stocks to short 3
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 = 31 #original is 30 but for consistency with factor make it 31
# Set the weights of each factor - see below original ratio all is 1 - correlation of each
self.beta_m = 2 #book value
self.beta_s = 1 #total equity
self.beta_h = 1 # op profit margin
self.beta_r = 1 # ROE total asset growth
self.beta_c = 2 # total asset growth
# # Risk model - additional parameter
stopRisk = self.GetParameter("stopRisk")
if stopRisk is None:
stopRisk = 0.16
self.SetRiskManagement(TrailingStopRiskManagementModel(float(stopRisk)))
#set SetBenchmark
self.SetBenchmark("SPY")
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 > 25], #share $ > 5 getting rid of penny stocks
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)
if holding.UnrealizedProfit > holding.Invested*.25: #set profit target as 25% of each securities
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)
self.EmitInsights(
# Creates an insight for our symbol, predicting that it will move up
Insight.Price(symbol, timedelta(self.rebalance_days), InsightDirection.Up)
)
# Open short position at the start of every month
for symbol in self.shortSymbols:
self.SetHoldings(symbol, -1/count)
self.EmitInsights(
# Creates an insight for our symbol, predicting that it will move down
Insight.Price(symbol, timedelta(self.rebalance_days), InsightDirection.Down)
)
# 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()