The strategy uses past equity and bond returns to guide investments in equity or bonds for 20 industrial countries. Positions are held for one month, targeting 10% volatility with equally-weighted country shares.

I. STRATEGY IN A NUTSHELL

This strategy uses bond and equity indexes from 20 leading industrial countries. Past 12-month returns are used as predictors: if equities fell and bonds rose, the investor goes long bonds; if both rose, the investor goes long equities. Otherwise, capital is held in a USD margin account earning the risk-free rate. Positions are held for one month with a target volatility of 10%, and country exposures are equally weighted.

II. ECONOMIC RATIONALE

The cross-asset momentum effect arises from slow-moving capital in bond and equity markets due to inattention, delays in decision-making, and market frictions. Positive bond returns reduce borrowing costs and increase collateral, allowing more investment in equities. Falling interest rates enable greater leverage for equities. Equity returns also influence future lending, affecting bond yields, while equity performance impacts bond returns through changes in the risk-free rate.

III. SOURCE PAPER

Cross-Asset Signals and Time Series Momentum [Click to Open PDF]

Pitkäjärvi, Vrije Universiteit Amsterdam, Tinbergen Institute; Suominen, Aalto University School of Business; Vaittinen, Independent

<Abstract>

We document a new phenomenon in bond and equity markets that we call cross-asset time series momentum. Using data from 20 countries, we show that past bond market returns are positive predictors of future equity market returns, and past equity market returns are negative predictors of future bond market returns. We use this predictability to construct a diversified cross-asset time series momentum portfolio that yields a Sharpe ratio 45% higher than a standard time series momentum portfolio. We present evidence that time series momentum and cross-asset time series momentum are driven by slow-moving capital in bond and equity markets.

IV. BACKTEST PERFORMANCE

Annualised Return6.5%
Volatility10%
Beta0.093
Sharpe Ratio 0.65
Sortino Ratio0.077
Maximum DrawdownN/A
Win Rate59%

V. FULL PYTHON CODE

import numpy as np
from AlgorithmImports import *
class GlobalCrossAssetTimeSeriesMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.symbols = {
            "ASX_YAP1" : "ASX_XT1",        # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1 (Australia)
            "LIFFE_FCE1" : "MX_CGB1",       # Ten-Year Government of Canada Bond Futures, Continuous Contract #1 (Canada)
            "EUREX_FSTX1" : "EUREX_FGBL1",  # Euro-Bund (10Y) Futures, Continuous Contract #1 (Germany)
            "SGX_NK1" : "SGX_JB1",          # SGX 10-Year Mini Japanese Government Bond Futures, Continuous Contract #1 (Japan)
            "LIFFE_Z1" : "LIFFE_R1",        # Long Gilt Futures, Continuous Contract #1 (U.K.)
            "CME_ES1" : "CME_TY1"           # 10 Yr Note Futures, Continuous Contract #1 (USA)
        }
        self.data = {}
        self.period = 12*21
        self.SetWarmUp(self.period)
        self.leverage_cap = 5
        
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            data = self.AddData(QuantpediaFutures, eq, Resolution.Daily)
            self.data[eq] = RollingWindow[float](self.period)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage_cap)
            
            data = self.AddData(QuantpediaFutures, bond, Resolution.Daily)
            self.data[bond] = RollingWindow[float](self.period)
            data.SetFeeModel(CustomFeeModel())
            data.SetLeverage(self.leverage_cap)
            
        first_key = [x for x in self.symbols.keys()][0]
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.MonthStart(first_key), self.TimeRules.At(0, 0), self.Rebalance)
    def OnData(self, data):
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            if eq in data and bond in data:
                if data[eq] and data[bond]:
                    eq_price = data[eq].Value
                    bond_price = data[bond].Value
                    if eq_price != 0 and bond_price != 0:
                        self.data[eq].Add(eq_price)
                        self.data[bond].Add(bond_price)
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        volatility = {}
        for eq in self.symbols:
            bond = self.symbols[eq]
            
            if all([self.data[x].IsReady and self.Securities[x].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x] for x in [eq, bond]]):
                eq_prices = np.array([x for x in self.data[eq]])
                bond_prices = np.array([x for x in self.data[bond]])
                
                eq_return = eq_prices[0] / eq_prices[-1] - 1
                bond_return = bond_prices[0] / bond_prices[-1] - 1
                
                if eq_return < 0 and bond_return > 0:
                    bond_returns = bond_prices[:-1] / bond_prices[1:] - 1
                    volatility[bond] = np.std(bond_returns) * np.sqrt(252)
                elif eq_return > 0 and bond_return > 0:
                    eq_returns = eq_prices[:-1] / eq_prices[1:] - 1
                    volatility[eq] = np.std(eq_returns) * np.sqrt(252)
        
        if len(volatility) == 0: return
        
        mean_vol = np.mean([x[1] for x in volatility.items()])
        # leverage = (0.0833 / total_vol_annualized) * 100
        leverage = min((0.1 / mean_vol), self.leverage_cap)
        
        self.Liquidate()
        count = len(volatility)
        
        for symbol in volatility:
            if data.contains_key(symbol) and data[symbol]:
            # self.SetHoldings(symbol, 0.1667 * (1/count) * leverage)
                self.SetHoldings(symbol, leverage / count)
    def Rebalance(self):
        self.rebalance_flag = True
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaFutures._last_update_date
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])
        if config.Symbol.Value not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
            QuantpediaFutures._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"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading