The Equity Differential Factor in Currencies
Log in to collectAcademic paper
The Equity Differential Factor in Currency Markets
David Turkington; Alireza Yazdani
- State Street (United States)
- ?David Turkington, CFA, is senior managing director at State Street Associates, Cambridge, Massachusetts
- ?Alireza Yazdani is vice president at State Street Associates, Cambridge, Massachusetts
https://www.tandfonline.com/doi/full/10.1080/0015198X.2020.1712924
Strategy in a nutshell
The strategy trades 45 currency pairs derived from G10 currencies. At the end of each month, it calculates the differential in trailing 12-month equity index total returns, expressed in local currency terms. Each currency pair is oriented to reflect a positive equity differential, ensuring that the long leg corresponds to the higher-return equity market. The strategy allocates equal weights across all pairs (or to a subset with the largest differentials) and nets exposures to determine final portfolio weights. Positions are implemented using one-month forward contracts, and the portfolio is rebalanced monthly.
Economic rationale
Instead of relying on traditional interest rate differentials, this strategy bases currency exposure on equity market performance. Currencies from countries with stronger recent equity returns tend to appreciate against those from weaker equity markets, forming the foundation of the "equity differential" approach. This relationship reflects the linkage between equity risk and currency risk—countries with higher equity premiums may exhibit stronger currencies as compensation for greater systematic risk. The effect is robust across samples and periods, suggesting that cross-country equity performance captures valuable information about relative economic strength and risk pricing in currency markets.
Backtest performance
Full Python code
from AlgorithmImports import *
class EquityDifferentialCurrencies(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2004, 1, 1)
self.SetCash(100000)
self.symbols = {
'CME_AD1' : 'ASX_YAP1',
'CME_BP1' : 'LIFFE_Z1',
'CME_CD1' : 'LIFFE_FCE1',
'CME_EC1' : 'EUREX_FSTX1',
'CME_JY1' : 'SGX_NK1',
'CME_SF1' : 'EUREX_FSMI1'
}
self.data = {}
self.period = 12*21
self.SetWarmUp(self.period)
self.leverage = 5
for symbol in self.symbols:
index = self.symbols[symbol]
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetLeverage(self.leverage)
data.SetFeeModel(CustomFeeModel())
self.AddData(QuantpediaFutures, index, Resolution.Daily)
self.data[index] = RollingWindow[float](self.period)
first_key = [x for x in self.symbols.keys()][0]
symbol = self.Symbol(self.symbols[first_key])
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthStart(symbol), self.TimeRules.At(0, 0), self.Rebalance)
def OnData(self, data):
for symbol in self.symbols:
index:str = self.symbols[symbol]
if index in data and data[index]:
price:float = data[index].Value
self.data[index].Add(price)
if not self.rebalance_flag:
return
self.rebalance_flag = False
if self.IsWarmingUp: return
index_return = {}
for symbol in self.symbols:
index = self.symbols[symbol]
if all([self.Securities[x].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[x] for x in [symbol, index]]):
if self.data[index].IsReady:
index_return[index] = self.data[index][0] / self.data[index][self.period-1] - 1
self.Liquidate()
if len(index_return) == 0: return
len_ = len(self.symbols)
count = (len_ * (len_-1)) * 2
for i in range(0, len(self.symbols)):
key_i = [x for x in self.symbols.keys()][i]
for j in range(i+1, len(self.symbols)):
key_j = [x for x in self.symbols.keys()][j]
if all([data.contains_key(symbol) and data[symbol] for symbol in [key_i, key_j]]):
eq_index1 = self.symbols[key_i]
eq_index2 = self.symbols[key_j]
if eq_index1 in index_return and eq_index2 in index_return:
if index_return[eq_index1] > index_return[eq_index2]:
self.SetHoldings(key_i, self.leverage*(1/count))
self.SetHoldings(key_j, -self.leverage*(1/count))
else:
self.SetHoldings(key_i, -self.leverage*(1/count))
self.SetHoldings(key_j, self.leverage*(1/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"))