FX forward returns #
The category group contains daily 1-month FX forward returns of tradeable, floating, and (largely) convertible currencies. By default, returns are calculated against their respective dominant cross, which will be the USD, EUR or an equally weighted basket of both. We also provide subcategories comprising returns against the USD only. All calculations assume monthly roll at the end of the month back to full 1-month maturity.
We calculate the FX forwards returns of going long the base currency , including the roll-down intra-monthly, in two steps. Firstly, we calculate the annualised carry as:
Where \(S_{t}\) is the FX spot price, \(F_{t}\) is the forward contract of tenor \(h\) (i.e. 1m forward: \(h=1/12\) ).
In the second stage, we use the no-arbitrage condition from Covered Interest Parity to calculate the forward price for each day (off the roll date), adjusting for days remaining in the month ( day-count-fraction : \({dcf}_{t}\) ):
Using the above, excess returns are then easily calculated as follows:
The FX spot and forward prices are from the JP Morgan trading desks, with Asian, European, and USDCAD taken at London close. Latin American FX crosses (USDBRL, USDCLP, USDCOP, USDMXN, and USDPEN) are end-of-day mark of each country’s trading desk.
FX forward return in % of notional #
Ticker : FXXR_NSA / FXXRUSD_NSA
Label : FX forward return, % of notional: dominant cross / against USD.
Definition : 1-month FX forward return, % of notional of the contract, assuming roll back to full 1-month maturity at the end of the month: long against natural benchmark currencies / long against USD.
Notes :
-
The default returns are calculated for a contract that is long the local currency of the cross section against its dominant traded benchmark.
-
The benchmark for Switzerland, Czechoslovakia, Hungary, Norway, Poland, Romania and Sweden is the euro, whilst Great Britain, Turkey and Russia use an equally weighted basket of dollars and euros. All other cross-sections use the dollar as a benchmark.
-
USD-compared returns are added separately for convenience should one wish to work with a pure USD-based panel.
-
For the following currencies, returns are at least partly based on non-deliverable contracts: Brazil, Chile, China, Indonesia, India, South Korea, Malaysia and Taiwan. Chile is deliverable as of 2021.
-
For some currencies, the returns include periods of low liquidity and FX targeting. If one wishes to ‘blacklist’ such periods, one should use the non-tradability and FX-target dummmies, which have category ticker codes
FXUNTRADABLE_NSA
andFXTARGETED_NSA
. Malaysia, as an example, is frequently blacklisted between 1999 and the end of 2007.
Vol-targeted FX forward return #
Ticker : FXXR_VT10 / FXXRUSD_VT10
Label : FX forward return for 10% vol target: dominant cross / against USD.
Definition : 1-month FX forward return, % of risk capital on position scaled to 10% (annualized) volatility target, assuming roll back to full 1-month maturity at the end of the month: long against natural benchmark currencies / long against USD.
Notes :
-
Positions are scaled to a 10% volatility target based on historic standard deviation for an exponential moving average with a half-life of 11 days. Positions are rebalanced at the end of each month. Moreover, a maximum leverage ratio of 5 (of implied notional to cash position) is imposed.
-
See the important related notes on “FX forward return in % of notional” above.
Hedged FX forward return #
Ticker : FXXRHvGDRB_NSA
Label : Return on FX forward, hedged against market direction risk.
Definition : Return on 1-month FX forward position that has been hedged against directional risk through a position in a global directional risk basket, % of forward notional, rolled at the end of the month.
Notes :
-
The global directional risk basket contains equal volatility-weights in equity index futures, CDS indices and FX forwards. See also the notes on the ‘directional risk basket returns’ category
DRBXR_NSA
here . -
Hedge ratios are calculated based on historical “beta”, i.e. weighted least-squares regression coefficients of past forward returns with respect to global directional risk basket returns. The estimate uses two regressions. One is based on monthly returns with an exponentially-weighted lookback of 24-months half-life. The other is based on daily returns with an exponentially-weighted lookback of 63 trading days. The usage of the two lookbacks seeks to strike a balance between timeliness of information and structural relations.
-
See the important related notes on “FX forward return in % of notional” above.
Imports #
Only the standard Python data science packages and the specialized
macrosynergy
package are needed.
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import math
import json
import yaml
import macrosynergy.management as msm
import macrosynergy.panel as msp
import macrosynergy.signal as mss
import macrosynergy.pnl as msn
from macrosynergy.download import JPMaQSDownload
from timeit import default_timer as timer
from datetime import timedelta, date, datetime
import warnings
warnings.simplefilter("ignore")
The
JPMaQS
indicators we consider are downloaded using the J.P. Morgan Dataquery API interface within the
macrosynergy
package. This is done by specifying
ticker strings
, formed by appending an indicator category code
<category>
to a currency area code
<cross_section>
. These constitute the main part of a full quantamental indicator ticker, taking the form
DB(JPMAQS,<cross_section>_<category>,<info>)
, where
<info>
denotes the time series of information for the given cross-section and category. The following types of information are available:
-
value
giving the latest available values for the indicator -
eop_lag
referring to days elapsed since the end of the observation period -
mop_lag
referring to the number of days elapsed since the mean observation period -
grade
denoting a grade of the observation, giving a metric of real time information quality.
After instantiating the
JPMaQSDownload
class within the
macrosynergy.download
module, one can use the
download(tickers,start_date,metrics)
method to easily download the necessary data, where
tickers
is an array of ticker strings,
start_date
is the first collection date to be considered and
metrics
is an array comprising the times series information to be downloaded.
# Cross-sections of interest
cids_dmca = [
"AUD",
"CAD",
"CHF",
"EUR",
"GBP",
"JPY",
"NOK",
"NZD",
"SEK",
"USD",
] # DM currency areas
cids_dmec = ["DEM", "ESP", "FRF", "ITL", "NLG"] # DM euro area countries
cids_latm = ["BRL", "COP", "CLP", "MXN", "PEN"] # Latam countries
cids_emea = ["CZK", "HUF", "ILS", "PLN", "RON", "RUB", "TRY", "ZAR"] # EMEA countries
cids_emas = [
"CNY",
# "HKD",
"IDR",
"INR",
"KRW",
"MYR",
"PHP",
"SGD",
"THB",
"TWD",
] # EM Asia countries
cids_dm = cids_dmca + cids_dmec
cids_em = cids_latm + cids_emea + cids_emas
cids = sorted(cids_dm + cids_em)
main = ["FXXR_NSA", "FXXR_VT10", "FXXRHvGDRB_NSA", "FXXRUSD_NSA", "FXXRUSD_VT10"]
econ = ["FXCRR_NSA", "FXCRR_V10", "FXCRRUSD_NSA", "FXCRRUSD_V10"]
mark = [
"EQXR_NSA",
"FXXRBETAvGDRB_NSA",
"FXTARGETED_NSA",
"FXUNTRADABLE_NSA",
] # related market categories
xcats = main + econ + mark
# Download series from J.P. Morgan DataQuery by tickers
start_date = "2000-01-01"
tickers = [cid + "_" + xcat for cid in cids for xcat in xcats]
print(f"Maximum number of tickers is {len(tickers)}")
# Retrieve credentials
client_id: str = os.getenv("DQ_CLIENT_ID")
client_secret: str = os.getenv("DQ_CLIENT_SECRET")
# Download from DataQuery
with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as downloader:
start = timer()
assert downloader.check_connection()
df = downloader.download(
tickers=tickers,
start_date=start_date,
metrics=["value", "eop_lag", "mop_lag", "grading"],
suppress_warning=True,
show_progress=True,
)
end = timer()
dfd = df
print("Download time from DQ: " + str(timedelta(seconds=end - start)))
Maximum number of tickers is 481
Downloading data from JPMaQS.
Timestamp UTC: 2023-09-06 13:32:07
Connection successful!
Number of expressions requested: 1924
Requesting data: 100%|█████████████████████████████████████████████████████████████████| 97/97 [00:30<00:00, 3.19it/s]
Downloading data: 100%|████████████████████████████████████████████████████████████████| 97/97 [00:57<00:00, 1.68it/s]
Download time from DQ: 0:01:48.251551
Availability #
cids_exp = sorted(list(set(cids) - set(cids_dmec))) # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df: []
Missing cids for FXXRHvGDRB_NSA: ['USD']
Missing cids for FXXRUSD_NSA: ['USD']
Missing cids for FXXRUSD_VT10: ['USD']
Missing cids for FXXR_NSA: ['USD']
Missing cids for FXXR_VT10: ['USD']
For most currencies, the return series are available back to 2000. Late starters are Romania, Russia and Indonesia, whose forward markets developed later.
For the explanation of currency symbols, which are related to currency areas or countries for which categories are available, please view Appendix 1 .
xcatx = main
cidx = cids_exp
dfx = msm.reduce_df(dfd, xcats=xcatx, cids=cidx)
dfs = msm.check_startyears(
dfx,
)
msm.visual_paneldates(dfs, size=(18, 4))
print("Last updated:", date.today())
Last updated: 2023-09-06
xcatx = main
cidx = cids_exp
plot = msm.check_availability(
dfd, xcats=xcatx, cids=cidx, start_size=(18, 3), start_years=False
)
xcatx = main
cidx = cids_exp
plot = msp.heatmap_grades(
dfd,
xcats=xcatx,
cids=cidx,
size=(18, 4),
title=f"Average vintage grades from {start_date} onwards",
)
xcatx = main
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
val="eop_lag",
title="End of observation period lags (ranges of time elapsed since end of observation period in days)",
start=start_date,
kind="box",
size=(16, 4),
)
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
val="mop_lag",
title="Median of observation period lags (ranges of time elapsed since middle of observation period in days)",
start=start_date,
kind="box",
size=(16, 4),
)
History #
FX forward returns in % of notional #
Long-term daily standard deviations of FX forward returns have been quite different, depending on exchange rate regimes, openness of the economies, and macroeconomic stability. Outliers have been common, with recorded daily returns of up to 40% The Turkish lira (TRY) posted the biggest gain in December 2021 after the introduction of a local-currency deposit insurance mechanism against exchange rate depreciation and accompanying interventions.
xcatx = ["FXXR_NSA"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="std",
start=start_date,
kind="box",
title="FX forward returns, % of notional, since 2000",
xcat_labels=["FX forward returns"],
size=(16, 8),
)
Naturally, FX forward positions are not a homogeneous asset class and long-term performances of the smaller currencies against USD and EUR has been very diverse. The below returns were not always reachable for international investors since they include periods of capital controls. To exclude these periods, one should include the information of the “FX tradeability and flexibility” category. Particularly striking examples include the performance of the Malaysian ringgit in the early 2000s and the recorded massive positive return on the Russian ruble in the initial phase of the Ukraine invasion.
xcatx = ["FXXR_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start=start_date,
title="Cumulative returns on 1-month forwards long in currency versus dominant benchmark",
title_fontsize=27,
title_adj=1.025,
title_xadj=0.5,
cumsum=True,
ncol=4,
same_y=False,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
Cross-sectional correlations across forward returns have been mostly positive. This should be expected since the U.S dollar is the common benchmark for most currencies and many EM currencies have been positively correlated with global risk returns. Two notable “negative correlators” have been CHF and JPY, both of which have historically posted negative correlations with directional market returns.
xcatx = "FXXR_NSA"
cidx = cids_exp
msp.correl_matrix(
dfd,
xcats=xcatx,
cids=cidx,
title="Cross-sectional correlations for FX forward returns, since 2000",
size=(20, 14),
)
Vol-targeted FX forward returns #
Standard deviations are similar across volatility targeted returns. However, outlier incidences have been more spectacular, with a short EURCHF forward posting record 70% daily returns. The proclivity to large outliers arises due the mechanical application of the volatility targeting. In periods of exchange rate targeting or pegging, they induce excessive leverage, leading to huge volatility targeted returns from the break of pegs.
xcatx = ["FXXR_VT10"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="std",
start=start_date,
kind="box",
title="Boxplots for 10% volatility-targeted FX forward returns, since 2000",
xcat_labels=["Vol-targeted FX forward returns"],
size=(16, 8),
)
Volatility targeting makes a substantial difference in the long-term cumulative returns of forwards, even if the volatility target is close to the historical volatility of the exchange rate. This reflects two effects:
-
Overtime risk of vol-targeted positions is reduced in times of turbulence and increased in quiet periods.
-
Positions in low-vol currencies are increased and those in high-vol countries are reduced.
xcatx = ["FXXR_VT10", "FXXR_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start=start_date,
title="Cumulative returns on 1-month forwards long in currency versus dominant benchmark",
xcat_labels=["Vol-targeted", "No targeting"],
title_adj=1.03,
title_xadj=0.46,
title_fontsize=27,
legend_fontsize=17,
label_adj=0.075,
cumsum=True,
ncol=4,
same_y=False,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
Hedged FX forward returns #
Hedging against global directional risk would have greatly reduced the long-term cumulative return on many carry trades. This reflects that carry currencies often incur a high “beta”, i.e. high dependency of global risk market returns. Put simply, the idiosyncratic premium on carry currencies is often quite modest.
xcatx = ["FXXRHvGDRB_NSA", "FXXR_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start=start_date,
title="Cumulative returns on 1-month forwards long in currency versus dominant benchmark",
xcat_labels=["Hedged returns", "No hedging"],
title_adj=1.03,
title_xadj=0.45,
title_fontsize=27,
legend_fontsize=17,
label_adj=0.075,
cumsum=True,
ncol=4,
same_y=False,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
Hedging FX forward positions against global directional risk would have reduced and evened out most cross-currency correlation coefficients.
xcatx = "FXXRHvGDRB_NSA"
cidx = cids_exp
msp.correl_matrix(
dfd,
xcats=xcatx,
cids=cidx,
title="Cross-sectional correlations for hedged FX forward returns",
size=(20, 14),
)
Importance #
Research Links #
“FX forward returns for 29 floating and convertible currencies since 1999 provide important empirical lessons. First, the long-term performance of FX returns has been dependent on economic structure and clearly correlated with forward-implied carry. The carry-return link has weakened considerably in the 2010s. Second, monthly returns for all currencies showed large and frequent outliers beyond the borders of a normal random distribution. Simple volatility targeting would not have mitigated this. Third, despite large fundamental differences, all carry and EM currencies have been positively correlated among themselves and with global risk benchmarks. Fourth, relative standard deviations across currencies have been predictable and partly structural. Hence, they have been important for scaling FX trades across small currencies.” Macrosynergy
Empirical Clues #
One of the most salient stylized features of past decades has been a long-term positive relation between real (inflation expectations-adjusted) forward-implied carry and FX forward returns.
dfb = dfd[dfd["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
:, ["cid", "xcat", "real_date", "value"]
]
dfba = (
dfb.groupby(["cid", "real_date"])
.aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
.reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
{'BRL': (Timestamp('2012-12-03 00:00:00'), Timestamp('2013-09-30 00:00:00')),
'CHF': (Timestamp('2011-05-02 00:00:00'), Timestamp('2016-06-30 00:00:00')),
'CNY': (Timestamp('2000-01-03 00:00:00'), Timestamp('2023-09-05 00:00:00')),
'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
'ILS': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-12-30 00:00:00')),
'INR': (Timestamp('2000-01-03 00:00:00'), Timestamp('2004-12-31 00:00:00')),
'MYR_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2007-11-30 00:00:00')),
'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2023-09-05 00:00:00')),
'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
'RON': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
'RUB_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2023-09-05 00:00:00')),
'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2023-09-05 00:00:00')),
'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
'TRY_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2003-09-30 00:00:00')),
'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2023-09-05 00:00:00'))}
xcatx = ["FXCRR_NSA", "FXXR_NSA"]
cidx = cids_exp
cr = msp.CategoryRelations(
dfd,
xcats=xcatx,
cids=cidx,
freq="A",
lag=0,
xcat_aggs=["mean", "sum"],
blacklist=fxblack,
start=start_date,
years=None,
)
FXCRR_NSA misses: ['USD'].
FXXR_NSA misses: ['USD'].
cr.reg_scatter(
title="FX real implied carry and returns across all countries and years since 2000",
labels=True,
coef_box="lower right",
xlab="Real FX forward-implied carry, % ar",
ylab="Cumulative 1-month forward return",
)
Real carry has also historically predicted subsequent monthly and quarterly FX forward returns.
xcatx = ["FXCRR_NSA", "FXXR_NSA"]
cidx = cids_exp
cr = msp.CategoryRelations(
dfd,
xcats=xcatx,
cids=cidx,
freq="M",
lag=1,
xcat_aggs=["mean", "sum"],
blacklist=fxblack,
start=start_date,
xcat_trims=[50, 50], # de-emphasize outliers
years=None,
)
FXCRR_NSA misses: ['USD'].
FXXR_NSA misses: ['USD'].
cr.reg_scatter(
title="FX real implied carry and subsequent returns, all countries since 2000",
labels=False,
coef_box="lower right",
xlab="Real FX forward-implied carry, % ar, month average",
ylab="Next month's 1-month FX forward return",
)
Appendices #
Appendix 1: Currency symbols #
The word ‘cross-section’ refers to currencies, currency areas or economic areas. In alphabetical order, these are AUD (Australian dollar), BRL (Brazilian real), CAD (Canadian dollar), CHF (Swiss franc), CLP (Chilean peso), CNY (Chinese yuan renminbi), COP (Colombian peso), CZK (Czech Republic koruna), DEM (German mark), ESP (Spanish peseta), EUR (Euro), FRF (French franc), GBP (British pound), HKD (Hong Kong dollar), HUF (Hungarian forint), IDR (Indonesian rupiah), ITL (Italian lira), JPY (Japanese yen), KRW (Korean won), MXN (Mexican peso), MYR (Malaysian ringgit), NLG (Dutch guilder), NOK (Norwegian krone), NZD (New Zealand dollar), PEN (Peruvian sol), PHP (Phillipine peso), PLN (Polish zloty), RON (Romanian leu), RUB (Russian ruble), SEK (Swedish krona), SGD (Singaporean dollar), THB (Thai baht), TRY (Turkish lira), TWD (Taiwanese dollar), USD (U.S. dollar), ZAR (South African rand).