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()