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:

(2)#\[\begin{equation} CRY_{t} = \left(\frac{S_{t}}{F_{t}}\right)^{1/h} - 1 \end{equation}\]

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}\)):

(3)#\[\begin{equation} {Fb}_{t} = S_{t} (1 + {CRY}_{t})^{-{dcf_{t}}} \end{equation}\]

Using the above, excess returns are then easily calculated as follows:

(4)#\[\begin{equation} {XR}_{t} = \begin{cases} \frac{Fb_{t}}{Fb_{t-1}} -1 & \forall\textrm{intra monthly}\\ \frac{Fb_{t}}{F_{t-1}} -1 & \forall\textrm{after a roll-date}. \end{cases} \end{equation}\]

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 and FXTARGETED_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())
../_images/FX forward returns_18_0.png
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
)
../_images/FX forward returns_19_0.png
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",
)
../_images/FX forward returns_20_0.png
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),
)
../_images/FX forward returns_21_0.png ../_images/FX forward returns_21_1.png

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),
)
../_images/FX forward returns_25_0.png

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,
)
../_images/FX forward returns_27_0.png

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),
)
../_images/FX forward returns_29_0.png

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),
)
../_images/FX forward returns_32_0.png

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,
)
../_images/FX forward returns_34_0.png

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,
)
../_images/FX forward returns_37_0.png

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),
)
../_images/FX forward returns_39_0.png

Importance#

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",
)
../_images/FX forward returns_47_0.png

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",
)
../_images/FX forward returns_50_0.png

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).