“Universe: NYSE, Amex, NASDAQ stocks. Sorted by size and investment independently. Intersected to form 25 portfolios. Long highest size, lowest investment; short highest size, highest investment. Value-weighted.”

I. STRATEGY IN A NUTSHELL

The investment universe consists of all NYSE, Amex, and NASDAQ stocks. Firstly, stocks are allocated to five Size groups (Small to Big) at the end of each June using NYSE market cap breakpoints. Stocks are allocated independently to five Investment (Inv) groups (Low to High) still using NYSE breakpoints. The intersections of the two sorts produce 25 Size-Inv portfolios. For portfolios formed in June of year t, Inv is the growth of total assets for the fiscal year ending in t-1 divided by total assets at the end of t-1. Long portfolio with the highest Size and simultaneously with the lowest Investment. Short portfolio with the highest Size and simultaneously with the highest Investment. The portfolios are value-weighted.

II. ECONOMIC RATIONALE

Past data and research proved that a conservative investment portfolio is connected with greater returns compared to an aggressive investment portfolio. To explain it briefly, aggressive investments do not improve returns in the near future, and yet it is questioned if those investments would improve returns in the future. Moreover, looking at the Size-Inv portfolios, estimates suggest that the five-factor model leaves only around 28% of the cross-section variance of expected returns unexplained. This is far less than the variance ratios produced by the Fama-French three-factor model, which are mostly greater than 50% for the Size-Inv portfolio. As a result, the investment factor could and should be used by investors.

III. SOURCE PAPER

A Five-Factor Asset Pricing Model [Click to Open PDF]

<Abstract>

A five-factor model directed at capturing the size, value, profitability, and investment patterns in average stock returns performs better than the three-factor model of Fama and French (FF 1993). The five-factor model’s main problem is its failure to capture the low average returns on small stocks whose returns behave like those of firms that invest a lot despite low profitability. The model’s performance is not sensitive to the way its factors are defined. With the addition of profitability and investment factors, the value factor of the FF three-factor model becomes redundant for describing average returns in the sample we examine.

IV. BACKTEST PERFORMANCE

Annualised Return3.54%
VolatilityN/A
Beta0.034
Sharpe Ratio0.363
Sortino Ratio0.385
Maximum Drawdown18.9%
Win Rate52%

V. FULL PYTHON CODE

from AlgoLib import *

class InvestmentFactor(XXX):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap

        self.long:List[Symbol] = []
        self.short:List[Symbol] = []

        self.quantile:int = 5
        self.leverage:int = 3
        self.rebalance_month:int = 6
        self.min_share_price:float = 1.
        
        self.weight:Dict[Symbol, float] = {}
        
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)

    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)

    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged

        selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 and x.AdjustedPrice >= self.min_share_price and \
            ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
            not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths) and not np.isnan(x.OperationRatios.TotalAssetsGrowth.OneYear) and \
            x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths != 0 and x.OperationRatios.TotalAssetsGrowth.OneYear != 0
            ]
        
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]

        # Sorting by investment factor.
        sorted_by_inv_factor:List[Fundamental] = sorted(selected, key = lambda x: (x.OperationRatios.TotalAssetsGrowth.OneYear / x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths), reverse=True)
        
        if len(sorted_by_inv_factor) >= self.quantile:
            quintile:int = int(len(sorted_by_inv_factor) / self.quantile)
            self.long = [x.Symbol for x in sorted_by_inv_factor[-quintile:]]
            self.short = [x.Symbol for x in sorted_by_inv_factor[:quintile]]
        
        return self.long + self.short
    
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False

        # order execution
        targets:List[PortfolioTarget] = []
        for i, portfolio in enumerate([self.long, self.short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)

        self.long.clear()
        self.short.clear()
    
    def Selection(self) -> None:
        if self.Time.month == self.rebalance_month:
            self.selection_flag = True
            
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading