FX tradeability and flexibility#

This category comprises time series’ of binary variables that indicate “untradability” in the main FX forward market or official targeting of the exchange rate. Lack of tradability, covertibility or flexibilty invalidates a time period for the backtesting of certain trading strategies. These periods should be “blacklisted” for the analysis of strategies that are require the local currency market to have some liquidity or flexibility.

Exchange rate target dummy#

Ticker: FXTARGETED_NSA

Label: Exchange rate target dummy.

Definition: Binary variable that takes the value 1 (rather than 0) if the exchange rate is targeted through a peg or any regime that signficantly reduces exchange rate flexibility.

Notes:

  • The exchange rate is considered targeted if the authorities have formally or informally set a peg, target corridor, cap or floor that is known in the market and expected to be defended by intervention and that substantially restricts the variation of the exchange rate.

  • The dummies are updated at the end of each month based on the assessment of J.P. Morgan market makers and/or research. For example, if a target has been announced in the middle of the month, the variable shifts from 0 to 1 at the end of the month.

  • Older history (prior to the launch of JPMaQS) is based on heritage data of Macrosynergy Partners and (prior to that) replication of real-time liquidity and flexibility status based on an expert review.

FX untradability dummy#

Ticker: FXUNTRADABLE_NSA

Label: FX untradability dummy

Definition: Binary variable that takes the value 1 (rather than 0) if either (i) liquidity in the main FX forward market is limited or (ii) convertibility restrictions signficantly distort the link between tradable offshore and untradable onshore contracts.

Notes:

  • Liquidity is considered limited if it is not possible to trade at least USD30 million in one ticket without significant market impact.

  • The dummies are updated once per month at the end of the month based on the assessment of J.P. Morgan market makers and/or research. Brief spells of illiquidity would not be captured by this series.

  • Older history (prior to launch of JPMaQS) is based on heritage data of Macrosynergy Partners and, before that, replication of real-time liquidity and flexibility status based on an expert review.

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 = ["FXTARGETED_NSA", "FXUNTRADABLE_NSA"]

econ = ["FXCRR_VT10", "FXCRR_NSA"]  # economic context

mark = ["FXXRxEASD_NSA", "FXXR_NSA", "FXXR_VT10"]  # market links

xcats = main + econ + mark
# Download series from J.P. Morgan DataQuery by tickers

start_date = "1995-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()
    df = downloader.download(
        tickers=tickers,
        start_date=start_date,
        metrics=["value", "eop_lag", "mop_lag", "grading"],
        suppress_warning=True,
    )
    end = timer()

dfd = df

print("Download time from DQ: " + str(timedelta(seconds=end - start)))
Maximum number of tickers is 259
Downloading data from JPMaQS.
Timestamp UTC:  2023-06-02 16:19:44
Connection successful!
Number of expressions requested: 1036
Download time from DQ: 0:00:57.368440

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:  set()
Missing cids for FXTARGETED_NSA:  {'USD'}
Missing cids for FXUNTRADABLE_NSA:  {'USD'}

JPMaQS holds information on FX tradeability and flexibility from 1999 for all currency areas.

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, 2))

print("Last updated:", date.today())
../_images/FX tradeability and flexibility_16_0.png
Last updated: 2023-06-02
xcatx = main
cidx = cids_exp

plot = msm.check_availability(
    dfd, xcats=xcatx, cids=cidx, start_size=(18, 4), start_years=False
)
../_images/FX tradeability and flexibility_17_0.png
xcatx = main
cidx = cids_exp

plot = msp.heatmap_grades(
    dfd,
    xcats=xcatx,
    cids=cidx,
    size=(18, 2),
    title=f"Average vintage grades from {start_date} onwards",
)
../_images/FX tradeability and flexibility_18_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 tradeability and flexibility_19_0.png ../_images/FX tradeability and flexibility_19_1.png

History#

Exchange rate target dummy#

Less than a third of the currencies in the JPMaQS sample have at any time been pegged or strictly targeted during the history recorded in JPMaQS. Only China and Singapore have been targeted throughout.

xcatx = ["FXTARGETED_NSA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Indicator variable for exchange rate targeting, since 1999",
    title_adj=1.02,
    ncol=4,
    same_y=True,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
../_images/FX tradeability and flexibility_23_0.png

Untradable FX dummy#

Over the last two decades, it has been rare to see sustained lack of tradability in the major FX forward markets. Malaysia and Turkey have become notable exceptions in the recent past.

xcatx = ["FXUNTRADABLE_NSA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Indicator variable for FX forward market untradeability, since 1999",
    title_adj=1.02,
    ncol=4,
    same_y=False,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
../_images/FX tradeability and flexibility_26_0.png

Importance#

Empirical clues#

Since 2000, roughly more than a third of the currencies covered by JPMaQS have seen their FX forward market affected either by illiquidity, an official exchange rate target, or convertibility-related distortions.

xcatx = ["FXTARGETED_NSA", "FXUNTRADABLE_NSA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Sustained market disruptions from FX targeting or illiquidity",
    title_adj=1.02,
    title_xadj=0.44,
    xcat_labels=["Targeted", "Untradeable"],
    ncol=7,
    same_y=True,
    size=(12, 10),
    aspect=1,
)
../_images/FX tradeability and flexibility_33_0.png

Blacklisting often gives a clearer picture of the statistical relations in the FX space. For example, it removes periods of “unattainable” carry, such as in Malaysia in the early 2000s, which would overstate the predictive power of FX carry for 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('1999-01-01 00:00:00'), Timestamp('2023-06-01 00:00:00')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
 'ILS': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-12-30 00:00:00')),
 'INR': (Timestamp('1999-01-01 00:00:00'), Timestamp('2004-12-31 00:00:00')),
 'MYR_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2007-11-30 00:00:00')),
 'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2023-06-01 00:00:00')),
 'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
 'RON': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2023-06-01 00:00:00')),
 'SGD': (Timestamp('1999-01-01 00:00:00'), Timestamp('2023-06-01 00:00:00')),
 'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
 'TRY_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2003-09-30 00:00:00')),
 'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2023-06-01 00:00:00'))}
xcatx = ["FXCRR_NSA", "FXXR_NSA"]
cidx = cids_exp

cr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    xcat_trims=[40, 30],
    start=start_date,
    years=None,
)

cr.reg_scatter(
    title="Real FX forward carry and subsequent cumulative returns without blacklisting across all markets since 2000",
    labels=False,
    coef_box="upper left",
    ylab="FX forward return next quarter, %",
    xlab="Real FX carry, % ar",
)
FXCRR_NSA misses: ['USD'].
FXXR_NSA misses: ['USD'].
../_images/FX tradeability and flexibility_36_1.png
xcatx = ["FXCRR_NSA", "FXXR_NSA"]
cidx = cids_exp

cr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    xcat_trims=[40, 30],
    blacklist=fxblack,
    start=start_date,
    years=None,
)

cr.reg_scatter(
    title="Real FX forward carry and subsequent cumulative returns with blacklisting across all markets since 2000",
    labels=False,
    coef_box="upper left",
    ylab="FX forward return next quarter, %",
    xlab="Real FX carry, % ar",
)
FXCRR_NSA misses: ['USD'].
FXXR_NSA misses: ['USD'].
../_images/FX tradeability and flexibility_37_1.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).