Quant BuffetRelax, Not Over Thinking

Modified volatility predicts DJI returns

Log in to collect

Academic paper

Strategy in a nutshell

Invest in a DJI-tracking instrument (e.g., DIA ETF) versus cash, allocating weights based on adjusted long-term volatility (ALTV) estimated via regressions of long-term and short-term volatility. Next-month returns are forecasted using ALTV, with portfolio weights determined by expected return, estimated variance, and a risk-aversion parameter. Portfolios are rebalanced monthly.

Economic rationale

ALTV provides strong out-of-sample forecasts of stock returns, aligning with the positive risk–return relationship predicted by asset pricing theory. It captures nuanced volatility–return dynamics, enhancing portfolio allocation decisions beyond traditional volatility measures.

Backtest performance

Annualised return3.23%
Beta0.037
Sortino ratio-0.466
Maximum drawdown0%
Win rate60%

Full Python code

from AlgorithmImports import *
from pandas.core.frame import DataFrame
import pandas as pd
import statsmodels.api as sm
# endregion

class ModifiedVolatilityPredictsDJIReturns(QCAlgorithm):

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

self.leverage:int = 3
self.equity:Symbol = self.AddEquity("DIA", Resolution.Minute, leverage=self.leverage).Symbol
self.cash:Symbol = self.AddEquity("BIL", Resolution.Minute, leverage=self.leverage).Symbol

self.gamma:float = 3.
self.estimation_period:int = 60
self.std_period:int = 24
self.months_in_year:float = 12.
self.allocation_cap:List[float] = [-0.5, 1.5]

self.recent_month:int = -1

def OnData(self, data: Slice) -> None:
if not(data.ContainsKey(self.equity) and data.ContainsKey(self.cash) and data[self.equity] and data[self.cash]):
    return

# monthly rebalance
if self.Time.month == self.recent_month:
    return
self.recent_month = self.Time.month

# get monthly closes
period:int = (self.estimation_period + self.std_period) * 2 + 1
history:DataFrame = self.History(self.equity, period * 31, Resolution.Daily)['close'].unstack(level=0)
history = history.groupby(pd.Grouper(freq='M')).last()[self.equity]

if len(history) >= period:
    history = history.iloc[-period:]
    returns:DataFrame = history.pct_change().iloc[1:]

    # long-term volatility 
    lv:DataFrame = returns.rolling(self.std_period).std() * np.sqrt(self.months_in_year)
    lv = lv.dropna()
    
    # short-term volatility 
    # SVAR is stock variance as in Welch and Goyal (2008)
    svar:DataFrame = (returns ** 2).rolling(self.std_period).sum()
    svar = svar.dropna()
    sv:DataFrame = np.sqrt(svar / self.months_in_year)

    lv_mean:DataFrame = lv.rolling(self.estimation_period).mean()
    lv_mean = lv_mean.dropna()
    sv_mean:DataFrame = sv.rolling(self.estimation_period).mean()
    sv_mean = sv_mean.dropna()

    # beta estimation
    beta_factor:np.ndarray = np.array([((lv.iloc[i-self.estimation_period:i] - lv_mean.iloc[i]) * (sv.iloc[i-self.estimation_period:i] - sv_mean.iloc[i])).sum() for i in range(-1, -(len(lv_mean)+1), -1)])
    beta_devisor:np.ndarray = np.array([((sv.iloc[i-self.estimation_period:i] - sv_mean.iloc[i]) ** 2).sum() for i in range(-1, -(len(lv_mean)+1), -1)])
    beta:np.ndarray = beta_factor / beta_devisor
    adj_lv:np.ndarray = (lv.iloc[-len(beta):].values - (beta * sv.iloc[-len(beta):].values))[-self.estimation_period:]

    # regression to predict return
    model = self.multiple_linear_regression(adj_lv[:-1], returns.iloc[-len(adj_lv):].values[1:])
    ret_pred:float = model.predict(adj_lv[-1])
    variance_pred:float = svar.iloc[-1] #lv.iloc[-1] ** 2

    # allocation
    equity_allocation:float = (ret_pred / (self.gamma * variance_pred))[-1]
    equity_allocation = min(max(equity_allocation, min(self.allocation_cap)), max(self.allocation_cap))
    cash_allocation:float = 1. - equity_allocation

    # trade execution
    self.SetHoldings(self.equity, equity_allocation)
    self.SetHoldings(self.cash, cash_allocation)
else:
    if self.Portfolio.Invested:
        self.Liquidate()

def multiple_linear_regression(self, x:np.ndarray, y:np.ndarray):
x:np.ndarray = np.array(x).T
# x = sm.add_constant(x)
result = sm.OLS(endog=y, exog=x).fit()
return result