The strategy uses intangible-adjusted book-to-market ratios (iB/M) to rank US stocks, going long on the top decile, short on the bottom, with value-weighted annual portfolios.

I. STRATEGY IN A NUTSHELL

The strategy trades U.S. stocks using intangible-adjusted book-to-market ratios, going long high iB/M stocks and short low iB/M stocks, holding value-weighted positions for one year.

II. ECONOMIC RATIONALE

Unrecorded intangible assets like R&D and brand value distort traditional book-to-market measures, creating opportunities for strategies that adjust for intangible capital to capture value-based returns.

III. SOURCE PAPER

An Intangible-adjusted Book-to-market Ratio Still Predicts Stock Returns [Click to Open PDF]

Hyuna Park

<Abstract>

The book-to-market ratio has been widely used to explain the cross-sectional variation in stock returns, but the explanatory power is weaker in recent decades than in the 1970s. I argue that the deterioration is related to the growth of intangible assets unrecorded on balance sheets. An intangible-adjusted ratio, capitalizing prior expenditures to develop intangible assets internally and excluding goodwill, outperforms the original ratio significantly. The average annual return on the intangible-adjusted high-minus-low (iHML) portfolio is 5.9% from July 1976 to December 2017 and 6.2% from July 1997 to December 2017, vs. 3.9% and 3.6% for an equivalent HML portfolio.

IV. BACKTEST PERFORMANCE

Annualised Return10.95%
Volatility15.22%
Beta0.04
Sharpe Ratio0.72
Sortino Ratio0.088
Maximum DrawdownN/A
Win Rate56%

V. FULL PYTHON CODE

from AlgorithmImports import *
from functools import reduce
from numpy import isnan
class ImpactOfIntangibleAssetsOnBM(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        # value weighted stocks
        self.weight:Dict[Symbol, float] = {}
        self.fundamental_count:int = 3000
        self.fundamental_sorting_key = lambda x: x.MarketCap
        self.financial_statement_names:List[str] = [
            'FinancialStatements.IncomeStatement.ResearchAndDevelopment.ThreeMonths',
            'FinancialStatements.IncomeStatement.SellingGeneralAndAdministration.ThreeMonths',
            'ValuationRatios.BookValuePerShare',
            'EarningReports.BasicAverageShares.ThreeMonths',
            'FinancialStatements.BalanceSheet.Goodwill.ThreeMonths',
            'MarketCap',
        ]
        self.quantile:int = 10
        self.leverage:int = 5
        self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
        self.last_year = -1
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        self.settings.daily_precise_end_time = False
    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.SecurityReference.ExchangeId in self.exchange_codes and \
            all((not isnan(self.rgetattr(x, statement_name)) and self.rgetattr(x, statement_name) != 0) for statement_name in self.financial_statement_names)]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
            
        iBE:Dict[Fundamental, float] = {}
        for stock in selected:
            Kcap:float = stock.FinancialStatements.IncomeStatement.ResearchAndDevelopment.ThreeMonths
            Ocap:float = stock.FinancialStatements.IncomeStatement.SellingGeneralAndAdministration.ThreeMonths
            # BE might be changed
            BE:float = stock.ValuationRatios.BookValuePerShare * stock.EarningReports.BasicAverageShares.ThreeMonths
            GoodWill:float = stock.FinancialStatements.BalanceSheet.Goodwill.ThreeMonths
            
            iBE[stock] = Kcap + Ocap + BE - GoodWill
        
        if len(iBE) >= self.quantile:
            sorted_by_iBE:List[Fundamental] = [x for x in sorted(iBE, key = iBE.get, reverse = True)]
            quantile:int = int(len(sorted_by_iBE) / self.quantile)
            # Long the top decile, short the bottom decile
            long:List[Fundamental] = sorted_by_iBE[:quantile]
            short:List[Fundamental] = sorted_by_iBE[-quantile:]
            
            # calculate weights
            for i, portfolio in enumerate([long, short]):
                mc_sum:float = sum(map(lambda x: x.MarketCap, portfolio))
                for stock in portfolio:
                    self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
        return list(self.weight.keys())
        
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade execution.
        portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
                        
    def Selection(self) -> None:
        if self.last_year != self.Time.year:
            self.last_year = self.Time.year
            self.selection_flag = True
    # https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288
    def rgetattr(self, obj, attr, *args):
        def _getattr(obj, attr):
            return getattr(obj, attr, *args)
        return reduce(_getattr, [obj] + attr.split('.'))
        
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading