
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.
ASSET CLASS: CFDs, forwards, futures | REGION: Global | FREQUENCY:
Weekly | MARKET: currencies | KEYWORD: Momentum, Spillover
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 Return | 2.2% |
| Volatility | 7.1% |
| Beta | 0.116 |
| Sharpe Ratio | 0.23 |
| Sortino Ratio | N/A |
| Maximum Drawdown | -25.2% |
| Win Rate | 43% |
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