
The investment universe consists of: GSCI Proxy (24 sub-indices [incl. CO COMDTY, GC COMDTY]) (representing Commodities), 16 developed and nine emerging markets (Equity Index [for example, SPX, NK, SX5E] constituents), Equity Single Stocks are 500 largest US stocks by market capitalization, G10 government bond [futures] (for Fixed Income), G10 currencies (as FX).
ASSET CLASS:CFDs, ETFs, forwards, futures, stocks, swaps | REGION: Global | FREQUENCY:
Quarterly | MARKET: bonds, commodities, currencies, equities | KEYWORD: Premia Strategy
I. 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.
II. 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.
III. SOURCE PAPER
Alternative Risk Premia Prime [Click to Open PDF]
Daniel Leveau, SigTech; Navdeep Sahote, SigTech
<Abstract>
The combination of last year’s large sell-off in the financial markets, a challenging macroeconomic environment, and heightened volatility has led institutional investors to reassess their strategic asset allocation. Guiding these reassessments is the central question of how best to fulfill the dual mandate of generating attractive returns, while providing downside protection for the portfolio.Hedge funds are an important component in institutional investors’ asset allocation. Indeed, several recent surveys indicate an expected increase in allocations to hedge funds and other alternative investment strategies in 2023, with investors increasingly adopting alternative risk premia (ARP) strategies as a substitute for traditional hedge funds. Buoyed by the global trend towards internalization, institutional investors are fashioning bespoke ARP strategies inhouse to profit from improved cost efficiency and increased transparency.This whitepaper explores the theoretical underpinnings of ARP strategies and their historical development. After discussing these fundamental principles, the report presents an empirical study of ARP across all major liquid asset classes.


IV. BACKTEST PERFORMANCE
| Annualised Return | 8.53% |
| Volatility | 6.62% |
| Beta | 0.178 |
| Sharpe Ratio | 1.23 |
| Sortino Ratio | N/A |
| Maximum Drawdown | -11.1% |
| Win Rate | 55% |
V. 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"))