Quant BuffetRelax, Not Over Thinking

Value Effect in Unprofitable Firms

Log in to collect

Academic paper

The Valuation of Loss Firms: A Stock Market Perspective

AuthorsHannes Mohrschladt; Susanne Siedhoff

Institute
  • DEUniversity of Münster
  • ?University of Muenster - Finance Center

Strategy in a nutshell

This strategy targets U.S. stocks with negative earnings. Stocks are sorted into quintiles based on the revenue-to-price (RP) ratio. The portfolio goes long on the highest RP stocks and short on the lowest RP stocks among loss firms. Portfolios are value-weighted by market capitalization and rebalanced monthly to capture cross-sectional return differences among loss-making companies.

Economic rationale

Loss firms are often harder to value due to weak earnings, leading to mispricing. Revenue-to-price and book-to-market ratios serve as predictive signals, as investor biases, informed trading, and limits to arbitrage amplify return spreads in these firms.

Backtest performance

Annualised return14.84%
Volatility36.55%
Beta0.198
Sharpe ratio0.41
Sortino ratio0.12
Win rate54%

Full Python code

from AlgorithmImports import *
# endregion

class ValueEffectInUnprofitableFirms(QCAlgorithm):

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

self.leverage:int = 5
self.quantile:int = 5

market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.weight:Dict[Symbol, float] = {}

self.fundamental_count:int = 1000
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market, 0), 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] = sorted([x for x in fundamental if x.HasFundamentalData and x.MarketCap and \
    ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
    x.FinancialStatements.IncomeStatement.TotalRevenue.ThreeMonths != 0
    and not np.isnan(x.FinancialStatements.IncomeStatement.TotalRevenue.ThreeMonths)], 
    key = lambda x: x.DollarVolume)[-self.fundamental_count:]

revenue_price_ratio:Dict[Fundamental, float] = { stock : (stock.FinancialStatements.IncomeStatement.TotalRevenue.ThreeMonths / stock.AdjustedPrice) \
    for stock in selected }

if len(revenue_price_ratio) < self.quantile:
    return Universe.Unchanged

quantile:int = int(len(revenue_price_ratio) / self.quantile)
sorted_by_ratio:List[Fundamental] = [x[0] for x in sorted(revenue_price_ratio.items(), key=lambda item: item[1])]
long:List[Fundamental] = sorted_by_ratio[-quantile:]
short:List[Fundamental] = sorted_by_ratio[:quantile]

for i, portfolio in enumerate([long, short]):
    mc_sum:float = sum(list(map(lambda stock: stock.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:
# rebalance monthly
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:
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"))