Multi Risk Premia Strategy
Log in to collectAcademic paper
Alternative Risk Premia Primer
Daniel Leveau; Navdeep Sahote
- ?Independent - affiliation not provided to SSRN
- ?SigTech
Strategy in a nutshell
Multi-asset universe (commodities, equities, bonds, FX). Five signals: Carry, Momentum, Tail Risk, Value, Volatility. Equal risk contribution, long top 25% / short bottom 25%, rebalanced quarterly.
Economic rationale
Carry: yield premium; Momentum: trend persistence; Tail Risk: crisis hedge; Value: mean reversion; Volatility: overpriced insurance. Low correlations (~0.05) → strong diversification, higher Sharpe.
Backtest performance
Annualised return8.53%
Volatility6.62%
Beta0.178
Sharpe ratio1.23
Maximum drawdown-11.1%
Win rate55%
Full Python code
from AlgorithmImports import *
from pandas.core.frame import DataFrame
#endregion
class MultiRiskPremiaStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
# Carry Quantpedia Strategies:
# ID 234 - Carry Factor within Asset Classes - 4 different universes
# Momentum Quantpedia Strategies:
# Commodities - ID 21
# Equity Index - ID 15
# Equity Single Stocks - ID 14
# Bonds - ID 426
# FX - ID 8
# Volatility Risk Strategies:
# Commodities - ID 506
# Equity Single Stocks - ID 20
# FX - ID 507
# Value Strategies:
# Commodities - ID 424
# FX - ID 9
# Equity Index - ID 26
# Bonds - ID 6
self.tickers:List[str] = [
'234_commodity', '234_equity',
'234_fx', '234_bonds',
'21', '15',
'14', '426',
'8', '506',
'20', '507',
'424', '6',
'9', '26',
]
self.volatility_period:int = 21
self.volatility_target:float = 0.1
self.leverage_cap:float = 5.
for ticker in self.tickers:
data:Security = self.AddData(QuantpediaEquity, ticker, Resolution.Daily)
data.SetLeverage(self.leverage_cap * 3)
data.SetFeeModel(CustomFeeModel())
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.recent_month:int = -1
def OnData(self, data:Slice) -> None:
if self.IsWarmingUp:
return
# quarterly rebalance
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
if self.Time.month % 3 != 0: return
# rebalance
_last_update_date:Dict[str, datetime.date] = QuantpediaEquity.get_last_update_date()
tickers_to_trade:List[str] = [ticker for ticker in self.tickers if \
ticker in _last_update_date and \
self.Time.date() < _last_update_date[ticker] and \
ticker in data and data[ticker]]
# inverse volatility weighting
long_count:int = len(tickers_to_trade)
price_df:DataFrame = self.History(tickers_to_trade, self.volatility_period, Resolution.Daily).unstack(level=0)
if not price_df.empty:
price_df = price_df['close']
daily_returns:DataFrame = price_df.pct_change().iloc[1:]
daily_returns = daily_returns.loc[:, (daily_returns != 0).any(axis=0)] # drop 0 columns
tickers_to_trade = list(map(lambda x: self.Symbol(x).Value, list(daily_returns.columns))) # updated valid columns
std:pd.Series = daily_returns.std()
weights:np.ndarray = ((1 / std) / (1 / std).sum()).values
# volatility target
portfolio_vol:float = np.sqrt(np.dot(weights.T, np.dot(daily_returns.cov() * self.volatility_period, weights.T)))
leverage:float = min(self.volatility_target / portfolio_vol, self.leverage_cap)
# trade execution
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for ticker in invested:
if ticker not in tickers_to_trade:
self.Liquidate(ticker)
for i, ticker in enumerate(tickers_to_trade):
self.SetHoldings(ticker, leverage * weights[i])
# Quantpedia strategy equity curve data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaEquity(PythonData):
def GetSource(self, config:SubscriptionDataConfig, date:datetime, isLiveMode:bool) -> SubscriptionDataSource:
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/911_related/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
_last_update_date:Dict[str, datetime.date] = {}
@staticmethod
def get_last_update_date() -> Dict[str, datetime.date]:
return QuantpediaEquity._last_update_date
def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLive: bool) -> BaseData:
data:config = QuantpediaEquity()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split:List[str] = line.split(';')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data['close'] = float(split[1])
data.Value = float(split[1])
# store last update date
if config.Symbol.Value not in QuantpediaEquity._last_update_date:
QuantpediaEquity._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
if data.Time.date() > QuantpediaEquity._last_update_date[config.Symbol.Value]:
QuantpediaEquity._last_update_date[config.Symbol.Value] = data.Time.date()
return data
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))