
The strategy involves sorting the 500 largest NYSE stocks by their Uncertainty of book-to-market ratios, taking long positions in the top decile and short positions in the bottom, with monthly rebalancing.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Value, Uncertainty
I. STRATEGY IN A NUTSHELL
Targets the 500 largest NYSE stocks, ranking them by UNC (standard deviation of expected book-to-market ratios). Goes long on the top decile and short on the bottom decile, using value-weighted portfolios rebalanced monthly.
II. ECONOMIC RATIONALE
High-UNC stocks carry greater productivity, consumption, and information risk, leading to higher expected returns. The premium arises from uncertainty and systematic risk exposure, with the strategy generating robust positive alphas in high-UNC stocks.
III. SOURCE PAPER
The Value Uncertainty Premium [Click to Open PDF]
Bali, Turan G.; Del Viv, Luca; El Hefnawy, Menatalla; Trigeorgis, Lenos — Georgetown University – McDonough School of Business; ESADE Business School; Universidad Complutense de Madrid (UCM) – Colegio Universitario de Estudios Financieros (CUNEF); University of Cyprus – Department of Public and Business Administration; King’s College London; Massachusetts Institute of Technology (MIT) – Sloan School of Management.
<Abstract>
We investigate whether time-series volatility in book-to-market (UNC) is priced in equity returns. UNC captures uncertainty about the current value of the firm’s portfolio of assets-in-place and real options, and reflects changes in moneyness and uncertainty in the exercise of these options. UNC is also associated with information risk, firm inflexibility, and investment volatility and commands a positive premium. An investment strategy long in high-UNC and short in low-UNC firms generates 13% annual risk-adjusted return. UNC premium is driven by outperformance of high-UNC (inflexible) firms facing higher information risk and is not explained by established risk factors and firm characteristics.


IV. BACKTEST PERFORMANCE
| Annualised Return | 12.01% |
| Volatility | 14.62% |
| Beta | 0.019 |
| Sharpe Ratio | 0.55 |
| Sortino Ratio | 0.199 |
| Maximum Drawdown | N/A |
| Win Rate | 50% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from numpy import isnan
from typing import List, Dict
class TheValueUncertaintyPremium(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2010, 1, 1)
self.SetCash(100_000)
self.exchange_codes: List[str] = ['NYS']
self.weight: Dict[Symbol, float] = {}
self.quantile: int = 10
self.leverage: int = 10
self.bm_by_symbol: Dict[Symbol, RollingWindow] = {}
self.m_period: int = 12
self.required_estimate_quarter: int = 4
self.universe_selection_period: int = 12
self.recent_EPS_estimate: Dict[Symbol, Dict[int, List[float]]] = {}
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.already_subscribed:list[Symbol] = []
self.last_fundamental: List[Symbol] = []
self.fundamental_count: int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.selection_flag: bool = False
self.rebalance_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
self.selection_flag = False
# select new universe once a period
if self.Time.month % self.universe_selection_period == 0:
selected: List[Fundamental] = [
x for x in fundamental
if x.HasFundamentalData
and x.Market == 'usa'
and x.MarketCap != 0
and x.SecurityReference.ExchangeId in self.exchange_codes
and not isnan(x.ValuationRatios.ForwardDividend) and x.ValuationRatios.ForwardDividend
and not isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0
and not isnan(x.EarningReports.BasicAverageShares.ThreeMonths) and x.EarningReports.BasicAverageShares.ThreeMonths != 0
and not isnan(x.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths) and x.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
for stock in selected:
symbol: Symbol = stock.Symbol
if symbol not in self.already_subscribed:
self.AddData(EstimizeEstimate, symbol)
self.already_subscribed.append(symbol)
self.last_fundamental = selected
self.rebalance_flag = True
UNC: Dict[Symbol, float] = {}
for stock in self.last_fundamental:
symbol: Symbol = stock.Symbol
# store pb value
if symbol not in self.bm_by_symbol:
self.bm_by_symbol[symbol] = RollingWindow[float](self.m_period)
# calculate UNC
if self.bm_by_symbol[symbol].IsReady:
bm_values: List[float] = [x for x in self.bm_by_symbol[symbol]]
bm_std: float = np.std(bm_values)
pb_mean: float = np.mean(bm_values)
UNC[stock] = bm_std / pb_mean
# get net income estimate
if symbol.Value in self.recent_EPS_estimate and self.Time.year in self.recent_EPS_estimate[symbol.Value]:
current_be: float = stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths - stock.FinancialStatements.BalanceSheet.CurrentLiabilities.ThreeMonths
# NOTE Expected net income for the end of fiscal year y, given the information available up to day d, is estimated as the product of expected earnings per share given by the mean of analysts’ forecasts up to day d and the total number of shares outstanding.
estimated_net_income: float = np.mean(self.recent_EPS_estimate[symbol.Value][self.Time.year]) * stock.EarningReports.BasicAverageShares.ThreeMonths
expected_dividend: float = stock.ValuationRatios.ForwardDividend
expected_be: float = current_be + estimated_net_income - expected_dividend
bm: float = expected_be / stock.MarketCap
self.bm_by_symbol[symbol].Add(bm)
else:
if self.bm_by_symbol[symbol].Count != 0:
self.bm_by_symbol[symbol].Reset()
if len(UNC) >= self.quantile:
sorted_by_perf: List[Tuple[Symbol, float]] = sorted(UNC.items(), key = lambda x:x[1], reverse=True)
quantile: int = int(len(sorted_by_perf) / self.quantile)
long: List[Fundamental] = [x[0] for x in sorted_by_perf[:quantile]]
short: List[Fundamental] = [x[0] for x in sorted_by_perf[-quantile:]]
# market cap weighting
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, slice: Slice) -> None:
# store latest EPS Estimize estimate
estimize: Dict[Symbol, float] = slice.Get(EstimizeEstimate)
for symbol, value in estimize.items():
ticker: str = symbol.Value
# store eond of the year estimates indexed by fiscal year
if value.FiscalQuarter == self.required_estimate_quarter:
if ticker not in self.recent_EPS_estimate:
self.recent_EPS_estimate[ticker] = {}
if value.FiscalYear not in self.recent_EPS_estimate[ticker]:
self.recent_EPS_estimate[ticker][value.FiscalYear] = []
self.recent_EPS_estimate[ticker][value.FiscalYear].append(value.Eps)
if not self.rebalance_flag:
return
self.rebalance_flag = False
# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if slice.contains_key(symbol) and slice[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: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))