Trade 45 currency pairs weekly based on 12-1 momentum of associated equity indices, going long on higher-momentum base currencies and short on lower-momentum quote currencies in equally weighted portfolios.

I. STRATEGY IN A NUTSHELL

Trade 45 currency pairs by comparing the 12-1 equity momentum of their linked national stock indices. Go long the base currency if its equity index shows stronger momentum than the quote currency’s index, and short otherwise. The portfolio is equally weighted and rebalanced weekly.

II. ECONOMIC RATIONALE

Strong equity performance attracts foreign capital, boosting demand for the country’s currency. This equity–currency spillover effect drives returns unexplained by traditional FX factors like carry or value, offering a unique source of momentum-based profits

III. SOURCE PAPER

Do Equities Spill Over to Currencies? [Click to Open PDF]

Philippe Declerck, HSBC Global Asset Management

<Abstract>

We document that equities indices spill over to currencies: cross-sectional momentum signals based on equities returns can help building investment strategies in the currencies space. Like momentum, this spillover effect tends to works better for short / mid term lookback periods, but spillover does not seem to be only a momentum phenomenon. Spillover is also robust to signals and portfolio construction modifications.

IV. BACKTEST PERFORMANCE

Annualised Return2.2%
Volatility7.1%
Beta0.116
Sharpe Ratio0.23
Sortino RatioN/A
Maximum Drawdown-25.2%
Win Rate43%

V. FULL PYTHON CODE

from AlgorithmImports import *
#endregion
class EquityMomentumSpillovertoCurrencies(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2007, 1, 1)
        self.SetCash(100000)
        
        # Country symbol and currency future symbol.
        self.symbols = {
                        "CME_CD1" : "LIFFE_FCE1",   # Canadian Dollar Futures, Continuous Contract #1
                        "CME_SF1" : "EUREX_FSMI1",  # Swiss Franc Futures, Continuous Contract #1
                        "CME_EC1" : "EUREX_FSTX1",  # Euro FX Futures, Continuous Contract #1
                        "CME_BP1" : "LIFFE_Z1",     # British Pound Futures, Continuous Contract #1
                        "CME_JY1" : "SGX_NK1",      # Japanese Yen Futures, Continuous Contract #1
                        }
        self.period = 12 * 21
        self.pairs = []
        self.data = {}  # momentum data
        self.max_missing_days = 5
        
        for i, curr_symbol1 in enumerate(self.symbols):
            # Equity index futures data.
            index_symbol = self.symbols[curr_symbol1]
            self.AddData(QuantpediaFutures, index_symbol, Resolution.Daily)
            self.data[index_symbol] = RollingWindow[float](self.period)
            
            # Currency futures data.
            if curr_symbol1 != "":  # except US dollar
                data = self.AddData(QuantpediaFutures, curr_symbol1, Resolution.Daily)
                data.SetLeverage(20)
                data.SetFeeModel(CustomFeeModel())
            else:
                continue
            for j, curr_symbol2 in enumerate(self.symbols):
                if j <= i: continue
                self.pairs.append((curr_symbol1, curr_symbol2))
        
    def OnData(self, data):
        # store daily equity index data
        for curr_symbol in self.symbols:
            index_symbol = self.symbols[curr_symbol]
            if curr_symbol in data and index_symbol in data and data[curr_symbol] and data[index_symbol]:
                index_price = data[index_symbol].Value
                self.data[index_symbol].Add(index_price)
        # weekly rebalance
        if self.Time.date().weekday() != 3:
            return
        # currency position
        curr_position = {}
        
        for pair in self.pairs:
            eq_symbol1 = self.symbols[pair[0]]
            eq_symbol2 = self.symbols[pair[1]]
            
            if not all(self.Securities[x].GetLastData() and (self.Time.date() - self.Securities[x].GetLastData().Time.date()).days <= self.max_missing_days for x in [eq_symbol1, eq_symbol2, pair[0], pair[1]]):
                continue
            # calculate equity index momentum
            if self.data[eq_symbol1].IsReady and self.data[eq_symbol2].IsReady:
                eq_momentum1 = self.data[eq_symbol1][21] / self.data[eq_symbol1][self.period-1] - 1
                eq_momentum2 = self.data[eq_symbol2][21] / self.data[eq_symbol2][self.period-1] - 1
                if pair[0] not in curr_position:
                    curr_position[pair[0]] = 0
                if pair[1] not in curr_position:
                    curr_position[pair[1]] = 0
                
                # add long pair position
                if eq_momentum1 > eq_momentum2:
                    curr_position[pair[0]] += 1
                    curr_position[pair[1]] -= 1
                # add short pair position
                else:
                    curr_position[pair[0]] -= 1
                    curr_position[pair[1]] += 1
        if len(curr_position) != 0:
            futures_invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
            for currency_future in futures_invested:
                if currency_future not in curr_position:
                    self.Liquidate(currency_future)
            
            total_position = sum([abs(x[1]) for x in curr_position.items()])
            for currency_future, country_signal in curr_position.items():
                self.SetHoldings(currency_future, country_signal)
        else:
            self.Liquidate()
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    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])
        return data

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