Stock and Bond Returns Predict Currency Returns
Log in to collectAcademic paper
Foreign Currency Forecasting: What Can Stock and Bond Markets Tell Us?
Kate Phylaktis; Ehab Abdel-Tawab Yamani
- City, University of London
- ?City University London - The Business School
- Chicago State University
Strategy in a nutshell
Universe: 28 currencies vs USD. Each month, use the past 120 months of equity- and bond-based predictors to forecast next-month currency returns. Long currencies with positive predicted returns, short those with negative predicted returns. Equally weighted, monthly rebalanced.
Economic rationale
Currency returns are linked to equity and bond market signals:
Return-chasing: strong local equity returns → currency appreciation.
Portfolio-rebalancing: strong equity returns → FX exposure reduced → depreciation.
Incorporating bonds (via UIP) enhances predictive power, exploiting cross-market information
Backtest performance
Annualised return8.47%
Volatility3.88%
Beta-0.009
Sharpe ratio2.11
Sortino ratio-0.3
Win rate49%
Full Python code
import statsmodels.api as sm
from AlgorithmImports import *
import data_tools
class TheSerialDependenceoftheCommodityFuturesReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
# equity etf and 10Y bond yield symbols by currency symbol
self.symbols = {
"AUDUSD" : ("EWA", "AU10YT"),
"CADUSD" : ("EWC", "CA10YT"),
"EURUSD" : ("EWG", "DE10YT"),
"JPYUSD" : ("EWJ", "JP10YT"),
"NZDUSD" : ("ENZL", "NZ10YT"),
"USDNOK" : ("ENOR", "NO10YT"),
"CHFUSD" : ("EWL", "CH10YT"),
"GBPUSD" : ("EWU", "GB10YT"),
"USDINR" : ("INDA", "IN10YT"),
"USDMXN" : ("EWW", "MX10YT"),
"USDPLN" : ("EPOL", "PL10YT"),
"USDSGD" : ("EWS", "SG10YT"),
"USDTRY" : ("TUR", "TR10YT"),
}
self.us_bonds = self.AddData(data_tools.QuantpediaBondYield, "US10YT", Resolution.Daily).Symbol
self.us_equities = self.AddEquity("VTI", Resolution.Daily).Symbol
self.daily_period = 21
self.period = 120 + 1
self.SetWarmUp(self.period * self.daily_period, Resolution.Daily)
self.perf_data = {}
self.last_month_price = {}
self.bond_yield_values = {}
for currency_symbol, (equity_symbol, bond_symbol) in self.symbols.items():
# currency
data = self.AddForex(currency_symbol, Resolution.Daily, Market.Oanda)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(5)
# equity
self.AddEquity(equity_symbol, Resolution.Daily)
# bond
self.AddData(data_tools.QuantpediaBondYield, bond_symbol, Resolution.Daily)
self.bond_yield_values[bond_symbol] = []
# price and performance data needed
symbols_needed = [currency_symbol, equity_symbol, bond_symbol]
for symbol in symbols_needed:
self.last_month_price[symbol] = None
self.perf_data[symbol] = RollingWindow[float](self.period)
self.last_month_price[self.us_equities] = None
self.perf_data[self.us_equities] = RollingWindow[float](self.period)
self.perf_data[self.us_bonds] = RollingWindow[float](self.period)
self.bond_yield_values[self.us_bonds] = []
self.recent_month = -1
def OnData(self, data):
# store daily bond yields
bond_yield_symbols = [x[1][1] for x in self.symbols.items()]
for bond_symbol in bond_yield_symbols + [self.us_bonds]:
if bond_symbol in data and data[bond_symbol]:
self.bond_yield_values[bond_symbol].append(data[bond_symbol].Value)
# rebalance once a month
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
if len(self.bond_yield_values[self.us_bonds]) == 0:
return
us_data_available = False
# store monthly performance for us bonds and equities
if self.Securities[self.us_equities].GetLastData() and (self.Time.date() - self.Securities[self.us_equities].GetLastData().Time.date()).days < 5 and \
self.Securities[self.us_bonds].GetLastData() and (self.Time.date() - self.Securities[self.us_bonds].GetLastData().Time.date()).days < 5:
us_eq_price = self.Securities[self.us_equities].Price
# symbol has last month's price stored
if self.last_month_price[self.us_equities] is not None:
self.perf_data[self.us_equities].Add(us_eq_price - self.last_month_price[self.us_equities])
# store monthly price
self.last_month_price[self.us_equities] = us_eq_price
# calculate monthly bond yield equity performance
monthly_us_bond_perf = self.bond_yield_equity_performance(np.array(self.bond_yield_values[self.us_bonds])) # take any count there is for a given month of daily bond yield values
self.perf_data[self.us_bonds].Add(monthly_us_bond_perf)
us_data_available = True
long = []
short = []
for currency_symbol, (equity_symbol, bond_symbol) in self.symbols.items():
# bond and equity data is still comming in
if self.Securities[currency_symbol].GetLastData() and (self.Time.date() - self.Securities[currency_symbol].GetLastData().Time.date()).days < 5 and \
self.Securities[bond_symbol].GetLastData() and (self.Time.date() - self.Securities[bond_symbol].GetLastData().Time.date()).days < 5 and \
self.Securities[equity_symbol].GetLastData() and (self.Time.date() - self.Securities[equity_symbol].GetLastData().Time.date()).days < 5:
symbols_needed = [currency_symbol, equity_symbol]
# store monthly prices and performance
for symbol in symbols_needed:
price = self.Securities[symbol].Price
# symbol has last month's price stored
if self.last_month_price[symbol] is not None:
self.perf_data[symbol].Add(price - self.last_month_price[symbol])
# store monthly price
self.last_month_price[symbol] = price
# bond yield value collection is not empty and it's size is equall to size of us bond yield values
if len(self.bond_yield_values[bond_symbol]) != 0:
# compute monthly bond yield equity performance
monthly_bond_perf = self.bond_yield_equity_performance(np.array(self.bond_yield_values[bond_symbol])) # take any count there is for a given month of daily bond yield values
self.perf_data[bond_symbol].Add(monthly_bond_perf)
symbols_needed += [bond_symbol]
# regression data is ready
if all(self.perf_data[symbol].IsReady for symbol in symbols_needed + [self.us_bonds, self.us_equities]) and us_data_available:
# linear regression
y = np.array([x for x in self.perf_data[currency_symbol]][:-1])
x = np.array([
[x for x in self.perf_data[bond_symbol]][1:],
[x for x in self.perf_data[self.us_bonds]][1:],
[x for x in self.perf_data[equity_symbol]][1:],
[x for x in self.perf_data[self.us_equities]][1:]
])
# prediction
model_result = self.multiple_linear_regression(x, y)
x_pred = np.array([
1,
self.perf_data[bond_symbol][0],
self.perf_data[self.us_bonds][0],
self.perf_data[equity_symbol][0],
self.perf_data[self.us_equities][0]
])
y_pred = model_result.predict(x_pred)[0]
if y_pred > 0:
# reverse position if needed
if currency_symbol[-3:] == 'USD':
long.append(currency_symbol)
else:
short.append(currency_symbol)
else:
# reverse position if needed
if currency_symbol[-3:] == 'USD':
short.append(currency_symbol)
else:
long.append(currency_symbol)
self.bond_yield_values[bond_symbol] = []
self.bond_yield_values[self.us_bonds] = []
if self.IsWarmingUp:
return
# trade execution
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long + short:
self.Liquidate(symbol)
for symbol in long:
self.SetHoldings(symbol, 1 / len(long))
for symbol in short:
self.SetHoldings(symbol, -1 / len(short))
def multiple_linear_regression(self, x:np.array, y:np.array):
x = x.T
x = sm.add_constant(x)
result = sm.OLS(endog=y, exog=x).fit()
return result
def bond_yield_equity_performance(self, bond_yield_values:np.array) -> float:
x:np.array = 1+bond_yield_values[:-1]/100/250
y:np.array = 7.7 * (bond_yield_values[:-1] - bond_yield_values[1:]) / 100
z:np.array = x+y
previous_equity:float = 1.0
eq:list = [previous_equity]
# calculate next equity value from the previous one
for value in z:
new_eq = value*previous_equity
eq += [new_eq]
previous_equity = new_eq
performance:float = (eq[-1] / eq[0] - 1)
return performance