Net Current Asset Value Effect
Log in to collectAcademic paper
Testing Benjamin Graham's Net Current Asset Value Strategy in London
Ying Xiao; Glen Arnold
- University of Salford
Strategy in a nutshell
The investment pool includes stocks from the London Exchange, omitting firms with multiple share classes, foreign entities, lightly regulated market participants, and financial sector businesses. Each July, a portfolio is assembled, selecting stocks boasting an NCAV/MV ratio above 1.5 for inclusion. This portfolio, adopting a Buy-and-hold strategy, remains unchanged for a year. Within this portfolio, all stocks are assigned equal weight.
Economic rationale
The NCAV rule creates portfolios consisting of stocks whose market capitalization is below their cash plus inventory value. Many of these stocks are priced low because they belong to companies facing financial difficulties, with a significant number on the brink of bankruptcy. However, a portion of these stocks, acquired at substantial discounts, tend to outperform, leading to statistically significant gains. Consequently, despite the inherent risks, the cumulative return of the portfolio tends to be overwhelmingly positive.
Backtest performance
Full Python code
from AlgoLib import *
import numpy as np
class NetCurrentAssetValueStrategy(XXX):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.UniverseSettings.Leverage = 3
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.SelectStocks)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
# Stock Selection Criteria
self.max_stocks = 3000
self.market = 'usa'
self.exclude_fin_sector = 103
self.ncav_ratio_min = 1.5
self.target_stocks = []
self.rebalance_period = 7
self.should_rebalance = True
self.spy = self.AddEquity('SPY', Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthStart(self.spy),
self.TimeRules.AfterMarketOpen(self.spy),
self.RebalancePortfolio)
def SelectStocks(self, fundamental_data: List[Fundamental]) -> List[Symbol]:
if not self.should_rebalance:
return Universe.Unchanged
qualified_stocks = [f for f in fundamental_data if f.HasFundamentalData and
f.Market == self.market and
f.AssetClassification.MorningstarSectorCode != self.exclude_fin_sector and
not np.isnan(f.MarketCap) and f.MarketCap > 0 and
not np.isnan(f.ValuationRatios.WorkingCapitalPerShare) and
f.ValuationRatios.WorkingCapitalPerShare > 0]
qualified_stocks = sorted(qualified_stocks,
key=lambda x: x.MarketCap)[:self.max_stocks]
self.target_stocks = [stock.Symbol for stock in qualified_stocks if
((stock.ValuationRatios.WorkingCapitalPerShare *
stock.EarningReports.BasicAverageShares.TwelveMonths) /
stock.MarketCap) > self.ncav_ratio_min]
return self.target_stocks
def OnData(self, data: Slice):
if not self.should_rebalance:
return
self.should_rebalance = False
if len(self.target_stocks) > 0:
weight = 1 / len(self.target_stocks)
for stock in self.target_stocks:
if data.ContainsKey(stock):
self.SetHoldings(stock, weight)
self.target_stocks.clear()
def RebalancePortfolio(self):
if self.Time.month == self.rebalance_period:
self.should_rebalance = True
def OnSecuritiesChanged(self, changes: SecurityChanges):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))