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).

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 Return8.53%
Volatility6.62%
Beta0.178
Sharpe Ratio1.23
Sortino RatioN/A
Maximum Drawdown-11.1%
Win Rate55%

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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading