Quant BuffetRelax, Not Over Thinking

Value Factor After Negative Market Return

Log in to collect

Academic paper

Value Bubbles

AuthorsMessaoud Chibane; Samuel Ouzan

Institute
  • NEOMA Business School
  • ?Neoma Business School

Strategy in a nutshell

“Cheap” stocks (high book-to-market ratio) and “expensive” stocks (low B/M)-ai identify panni, market periya maari down irundha, cheap stocks-ai vaangi, expensive stocks-ai sell pannu. Positions 6 months hold pannuvom.

Economic rationale

Oru vela investors overreact panni expensive stocks buy pannuvanga; cheap stocks ignore pannuvanga. Market calm-a irundha, cheap stocks-u better returns koduthu, overpriced stocks-u avoid pannumbothu safe-a irukkum.

Backtest performance

Annualised return3.68%
Volatility3.45%
Beta0.104
Sharpe ratio1.07
Sortino ratio-0.113
Win rate52%

Full Python code

from AlgorithmImports import *
from numpy import floor
#endregion
class ValueFactorAfterNegativeMarketReturn(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.coarse_count = 500

self.period = 24 * 21
self.SetWarmUp(self.period, Resolution.Daily)
self.quantile = 10
# Trenching
self.holding_period = 6
self.managed_queue = []

self.symbol = self.AddEquity('VTI', Resolution.Daily).Symbol
self.data = RollingWindow[float](self.period)
self.selection_flag = False

self.recent_price = {}  # recent stock prices

self.UniverseSettings.Resolution = Resolution.Daily
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(10)
def CoarseSelectionFunction(self, coarse):
for stock in coarse:
    symbol = stock.Symbol
    # append recent price to market symbol
    if symbol == self.symbol:
        self.data.Add(stock.AdjustedPrice)
    # store monthly stock prices
    if self.selection_flag:
        self.recent_price[symbol] = stock.AdjustedPrice
    
if not self.selection_flag:
    return Universe.Unchanged
selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
    key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
return [x.Symbol for x in selected]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and x.ValuationRatios.PBRatio != 0]

# market data is ready
if self.data.IsReady:
    market_ret:float = self.data[0] / self.data[self.period-1] - 1
    if market_ret >= 0:
        return Universe.Unchanged
else:
    return Universe.Unchanged
bm_ratio = {}
market_cap = {}
for stock in fine:
    symbol = stock.Symbol
    
    market_cap[symbol] = stock.MarketCap
    bm_ratio[symbol] = 1 / stock.ValuationRatios.PBRatio

long:list = []
short:list = []

if len(bm_ratio) >= self.quantile:
    # BM ratio sorting 
    sorted_by_bm = sorted(bm_ratio.items(), key=lambda x: x[1], reverse=True)
    decile = int(len(sorted_by_bm) / self.quantile)
    
    # Long the highest decile portfolio 
    # Short the lowest decile portolio
    long = [x[0] for x in sorted_by_bm[:decile]]
    short = [x[0] for x in sorted_by_bm[-decile:]]

    # Market cap weighting
    equity = self.Portfolio.TotalPortfolioValue / self.holding_period
    
    weights = {}
    
    total_market_cap_long = sum([market_cap[sym] for sym in long if sym in market_cap])
    for symbol in long:
        if symbol in market_cap:
            weights[symbol] = market_cap[symbol] / total_market_cap_long
    
    total_market_cap_short = sum([market_cap[sym] for sym in short if sym in market_cap])
    for symbol in short:
        if symbol in market_cap:
            weights[symbol] = -market_cap[symbol] / total_market_cap_short
    
    symbol_q = [(symbol, floor((equity*symbol_w) / self.recent_price[symbol])) for symbol,symbol_w in weights.items()]
    self.managed_queue.append(RebalanceQueueItem(symbol_q))
        
return long + short
def OnData(self, data):
if not self.selection_flag:
    return
self.selection_flag = False

# Trade execution
remove_item = None

# Rebalance portfolio
for item in self.managed_queue:
    if item.holding_period == self.holding_period:
        for symbol, quantity in item.symbol_q:
            self.MarketOrder(symbol, -quantity)
                    
        remove_item = item
        
    elif item.holding_period == 0:
        open_symbol_q = []
        
        for symbol, quantity in item.symbol_q:
            if symbol in data and data[symbol]:
                self.MarketOrder(symbol, quantity)
                open_symbol_q.append((symbol, quantity))
        
        # Only opened orders will be closed        
        item.symbol_q = open_symbol_q
        
    item.holding_period += 1
    
# We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
if remove_item:
    self.managed_queue.remove(remove_item)
def Selection(self):
self.selection_flag = True
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q  
self.holding_period = 0
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))