Quant BuffetRelax, Not Over Thinking

Stock and Bond Returns Predict Currency Returns

Log in to collect

Academic paper

Foreign Currency Forecasting: What Can Stock and Bond Markets Tell Us?

AuthorsKate Phylaktis; Ehab Abdel-Tawab Yamani

Institute
  • 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