The strategy uses relationships between currency forwards, equity indices, and bond futures, allocating risk equally across asset classes, optimizing weights, and rebalancing weekly based on spillovers and cumulative returns over 3-4 years.

I. STRATEGY IN A NUTSHELL

The strategy trades currency forwards, equity indices, and bond futures across nine exchange rates, 11 developed-country equity indices, and multiple bond markets. It models inter-asset relationships: bonds negatively affect FX and positively affect equities; equities negatively affect both bonds and FX; FX positively affects both bonds and equities. Signals are derived from 3–4 year cumulative returns, and one-third of the risk budget is allocated to each asset class. Weights are optimized via the sum of logarithms of absolute weights, considering six spillover scenarios. The portfolio is rebalanced weekly.

II. ECONOMIC RATIONALE

The strategy exploits spillover effects: bonds support equities via lower rates but depress FX through USD appreciation; equities signal inflation, negatively affecting bonds; FX movements influence both bonds and equities. While not all relationships are observable, three key spillovers—bonds to equities, equities to FX, and FX to equities—drive profitability. The composite strategy leverages these interconnections for consistent returns.

III. SOURCE PAPER

Trend-Following and Spillover Effects [Click to Open PDF]

Declerck, Philippe, HSBC Global Asset Management

<Abstract>

We start by documenting trend-following (or time series momentum) in government bond, currency and equity index (all developed countries) at the asset class level, and at the multi-asset level, using 29 liquid instruments, with lookback periods ranging from 1 to 60 months. A typical multi-asset trend-following strategy delivers strong returns for short to medium term lookback periods. I document that trends spill over to other asset classes: past trends of assets can help to build investment strategies using other related assets. This spillover effect works better when using longer lookback periods than the sweet spot for trend-following.

IV. BACKTEST PERFORMANCE

Annualised Return3.2%
Volatility4.8%
Beta-0.001
Sharpe Ratio0.67
Sortino Ratio-0.811
Maximum Drawdown-12.3%
Win Rate53%

V. FULL PYTHON CODE

from AlgorithmImports import *
import numpy as np
class TrendFollowingandSpilloverEffect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        # Symbols - currency, index and bond futures.
        self.symbols = [
            ('CME_AD1', 'ASX_YAP1', 'ASX_XT1'),          # Australian Dollar Futures, Continuous Contract #1
            ('CME_BP1', 'LIFFE_Z1', 'LIFFE_R1'),         # British Pound Futures, Continuous Contract #1
            ('CME_CD1', 'LIFFE_FCE1', 'MX_CGB1'),        # Canadian Dollar Futures, Continuous Contract #1
            ('CME_EC1', 'EUREX_FSTX1', 'EUREX_FGBL1'),   # Euro FX Futures, Continuous Contract #1
            ('CME_JY1', 'SGX_NK1', 'SGX_JB1'),           # Japanese Yen Futures, Continuous Contract #1
            ('CME_DX1', 'CME_ES1', 'CME_TY1')           # US Dollar Index Futures, Continuous Contract #1
            # ('CME_SF1', 'EUREX_FSMI1', '')            # Swiss Franc Futures, Continuous Contract #1
            # ('CME_MP1', '', '')                       # Mexican Peso Futures, Continuous Contract #1
            # ('CME_NE1', '', '')                       # New Zealand Dollar Futures, Continuous Contract #
        ]
        self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        # Daily ROC data.
        self.data = {}
       
        self.period = 36 * 21
        self.SetWarmUp(self.period)
        
        for futures_symbols in self.symbols:
            for symbol in futures_symbols:
                data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
                
                self.data[symbol] = SymbolData(self.period)
                
                data.SetFeeModel(CustomFeeModel())
                data.SetLeverage(5)
            
        self.rebalance_flag: bool = False
        self.Schedule.On(self.DateRules.WeekStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Rebalance)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
    
    def OnData(self, data):
        for futures_symbols in self.symbols:
            for symbol in futures_symbols:
                if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]:
                    self.liquidate(symbol)
                    self.data[symbol].reset()
                    continue
                if symbol in data and data[symbol]:
                    price = data[symbol].Value
                    self.data[symbol].update(price)
        if not self.rebalance_flag:
            return
        self.rebalance_flag = False
        weight = {}
        traded_asset_classs_count = 0
        
        for futures_symbols in self.symbols:
            fx = futures_symbols[0]
            eq = futures_symbols[1]
            bond = futures_symbols[2]
            
            if self.data[fx].is_ready() and self.data[eq].is_ready() and self.data[bond].is_ready():
                fx_perf = self.data[fx].performance()
                eq_perf = self.data[eq].performance()
                bond_perf = self.data[bond].performance()
                
                bond_w = 0
                fx_w = 0
                eq_w = 0
                
                # Bonds have a negative effect on FX and positive effect on Equities
                bond_signum = np.sign(bond_perf)
                fx_w -= bond_signum
                eq_w += bond_signum
                
                # Equities have a negative effect on Bonds and negative effect on FX
                eq_signum = np.sign(eq_perf)
                bond_w -= eq_signum
                fx_w -= eq_signum
                
                # FX has a positive effect on Equities and positive effect on Bonds
                fx_signum = np.sign(fx_perf)
                eq_w += fx_signum
                bond_w += fx_signum
                # inverse volatility sum of traded symbols
                total_volatility = sum([ 1/self.data[x[0]].volatility() for x in [(fx,fx_w), (eq,eq_w), (bond, bond_w)] if x[1] != 0 ])
                
                # volatility weighting
                if total_volatility != 0:
                    weight[fx] = ((1/self.data[fx].volatility()) / total_volatility) * np.sign(fx_w)
                    weight[eq] = ((1/self.data[eq].volatility()) / total_volatility) * np.sign(eq_w)
                    weight[bond] = ((1/self.data[bond].volatility()) / total_volatility) * np.sign(bond_w)
                
                traded_asset_classs_count += 1
            
        portfolio: List[PortfolioTarget] = []
        if traded_asset_classs_count != 0:
            weight_ratio = 1 / traded_asset_classs_count
            portfolio = [PortfolioTarget(symbol, weight_ratio * w) for symbol, w in weight.items() if data.contains_key(symbol) and data[symbol]]
        
        self.SetHoldings(portfolio, True)
    def Rebalance(self):
        self.rebalance_flag = True
class SymbolData():
    def __init__(self, period):
        self.price = RollingWindow[float](period)
        self.period = period
        
    def update(self, value) -> None:
        self.price.Add(value)
    
    def performance(self) -> float:
        result = self.price[0] / self.price[self.period-1] - 1
        return result
    def volatility(self) -> float:
        prices = np.array([x for x in self.price][:60])
        result = prices[:-1] / prices[1:] - 1
        result = np.std(result) * np.sqrt(252)
        return result
        
    def reset(self) -> None:
        self.price.reset()
    def is_ready(self) -> bool:
        return self.price.IsReady
        
# 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