FX tail risk premia #
The category measures option-implied premia of the tail risk in FX forwards. The premium is calculated as the difference between the implied volatility of out-of-the-money options and at-the-money options, divided by the volatility of at-the-money options. A high ratio implies that the implied volatility of the out-of-the money option is larger and that buying protection against movements to the tail of return distribution is disproportionately expensive.
The out-of-the-money part of the ratio is based on 10-delta options. A 10-delta implied volatility is the implied volatility of an option that has roughly a 10% probability to be in the money. It is an option with a low likelihood of being exercised. There are two types of tail risk premium:
-
The downside tail risk premium is charged for the risk of large depreciation of the local currency versus the benchmark. It is calculated as the implied volatility of a 10-delta put (benchmark call) to the implied volatility of an at-the-money option of the same type.
-
The upside tail risk premium is charged for the risk of large appreciation of the local currency versus the benchmark. It is calculated as the ratio of implied volatility of a 10-delta call (benchmark put) to the implied volatility of an at-the-money option of the same type.
Imports #
Only the standard Python data science packages and the specialized
macrosynergy
package are needed.
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 os
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.
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
vdic = {
"EUR": ["NOK", "SEK", "CHF", "CZK", "HUF", "PLN", "RON", "GBP", "TRY", "RUB"],
"USD": [
"CAD",
"CHF",
"GBP",
"EUR",
"NZD",
"AUD",
"SEK",
"NOK",
"JPY",
"ISK",
"DKK",
"CZK",
"RUB",
"RON",
"TRY",
"ZAR",
"ILS",
"HUF",
"PLN",
"PHP",
"SGD",
"THB",
"HKD",
"ARS",
"BRL",
"CLP",
"COP",
"PEN",
"MXN",
"MYR",
"TWD",
"INR",
"IDR",
"KRW",
"CNY",
],
}
cids_dm = cids_dmca + cids_dmec
cids_em = cids_latm + cids_emea + cids_emas
cids = sorted(cids_dm + cids_em)
main = [
"FXPPTAILDOWN_NSA",
"FXPPTAILUP_NSA",
"FXPPTAILDOWNUSD_NSA",
"FXPPTAILUPUSD_NSA",
]
econ = ["FXXRxEASD_NSA", "BXBGDPRATIO_NSA_12MMA"] # economic context
mark = [
"EQXR_NSA",
"EQXR_VT10",
"FXXR_NSA",
"FXXR_VT10",
"FXTARGETED_NSA",
"FXUNTRADABLE_NSA",
] # market links
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,
)
end = timer()
dfd = df
print("Download time from DQ: " + str(timedelta(seconds=end - start)))
Maximum number of tickers is 456
Downloading data from JPMaQS.
Timestamp UTC: 2024-02-22 17:44:55
Connection successful!
Some expressions are missing from the downloaded data. Check logger output for complete list.
348 out of 1824 expressions are missing. To download the catalogue of all available expressions and filter the unavailable expressions, set `get_catalogue=True` in the call to `JPMaQSDownload.download()`.
Some dates are missing from the downloaded data.
2 out of 6301 dates are missing.
Download time from DQ: 0:00:58.817074
Availability #
cids_exp = sorted(
list(set(cids) - set(cids_dmec) - set(["HKD"]))
) # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df: []
Missing cids for FXPPTAILDOWNUSD_NSA: ['USD']
Missing cids for FXPPTAILDOWN_NSA: ['USD']
Missing cids for FXPPTAILUPUSD_NSA: ['USD']
Missing cids for FXPPTAILUP_NSA: ['USD']
Whilst most quantamental indicators of FX tail risk premia are available from 2000, including all those for developed markets, some emerging market series’ are only available from the mid-2000s onwards.
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, 3))
print("Last updated:", date.today())
Last updated: 2024-02-22
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, 3),
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 tail risk premium against dominant benchmark #
Downside risk premia have on average been positive for all non-USD currencies since 2000. They have been the largest in some EM currencies with historically low spot-exchange rate volatility in normal times, such as Peru and Romania. This is likely related to the risk of breaks in managed FX regimes. Upside risk premia have been more balanced and closer to zero, with the notable exceptions of CHF, JPY and CNY. CHF, JPY and GBP have been the oly currencies with higher average upside risk premia and downside risk premia.
xcatx = ["FXPPTAILDOWN_NSA", "FXPPTAILUP_NSA"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="mean",
start=start_date,
title="Means and standard deviations of FX tail risk premia vs dominant benchmark, since 2000",
xcat_labels=["Downside risk", "Upside risk"],
kind="bar",
size=(16, 8),
)
xcatx = ["FXPPTAILDOWN_NSA", "FXPPTAILUP_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start=start_date,
title="FX tail risk protection premia, local currency vs main benchmark",
xcat_labels=["Downside risk", "Upside risk"],
ncol=4,
same_y=False,
label_adj=0.05,
aspect=1.7,
) # , all_xticks=False)
Downside risk premia have been positively correlated across most exchange rate pairs.
xcatx = "FXPPTAILDOWN_NSA"
cidx = cids_exp
dfd_vrp7 = dfd[dfd["xcat"] == "FXPPTAILDOWN_NSA"][
["real_date", "cid", "xcat", "value"]
].set_index("real_date")
dfw_vrp7 = dfd_vrp7.groupby(["cid", "xcat"]).resample("M").mean().reset_index()
msp.correl_matrix(
dfw_vrp7,
xcats=xcatx,
cids=cidx,
size=(20, 14),
title="Cross-sectional correlations for downside risk premia since 2000",
cluster=True,
)
FX tail risk premium against USD #
As for dominant benchmarks, downside tail risk premia have been positive for all currencies against the USD.
xcatx = ["FXPPTAILDOWNUSD_NSA", "FXPPTAILUPUSD_NSA"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="mean",
start=start_date,
title="Means and standard deviations of FX tail risk premia vs USD benchmark, since 2000",
xcat_labels=["Downside risk", "Upside risk"],
kind="bar",
size=(16, 8),
)
xcatx = ["FXPPTAILDOWNUSD_NSA", "FXPPTAILUPUSD_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start=start_date,
title="FX tail risk protection premia, local currency vs USD benchmark",
title_adj=0.95,
xcat_labels=["Downside risk", "Upside risk"],
ncol=4,
same_y=False,
label_adj=0.05,
aspect=1.7,
all_xticks=False,
)
Importance #
Research Links #
“(FX) tail risk is mostly influenced by the long position of the carry trade… Historically, Carry trades have been a success story for most investors and a major source of funds for emerging economies maintaining higher interest rates.” Ganepola
“We propose a new decomposition of the variance risk premium in terms of upside and downside variance risk premia. These components reflect market compensation for changes in good and bad uncertainties. Their difference is a measure of skewness risk premium, which captures investors’ asymmetric views on favorable versus undesirable risks.” Feunou, Jahan-Parvar, Okou
Empirical Clues #
There is some evidence of positive predictive power in downside risk premia for subsequent FX returns. This suggests that on balance investors have been paid for receiving such premia.
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-10-03 00:00:00'), Timestamp('2015-01-30 00:00:00')),
'CNY': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-02-21 00:00:00')),
'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
'HKD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-02-21 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('2024-02-21 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('2024-02-21 00:00:00')),
'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-02-21 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('2024-02-21 00:00:00'))}
xcatx = ["FXPPTAILDOWN_NSA", "FXXR_VT10"]
cidx = cids_exp
cr = msp.CategoryRelations(
dfd,
xcats=xcatx,
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"], # xcat1_chg="pch", fwin=4,
start="2000-01-01",
blacklist=fxblack,
)
cr.reg_scatter(
title="Downside protection premium and subsequent returns, global panel since 2000",
labels=False,
coef_box="upper right",
xlab="Downside tail risk premium (end of quarter)",
ylab="Cumulative vol-targeted FX forward return, next quarter",
)
FXPPTAILDOWN_NSA misses: ['USD'].
FXXR_VT10 misses: ['USD'].
xcatx = ["FXPPTAILDOWN_NSA", "FXXR_VT10"]
cidx = cids_exp
sr = mss.SignalReturnRelations(
dfd,
rets=xcatx[1],
sigs=xcatx[0],
agg_sigs="mean",
cids=cids_exp,
blacklist=fxblack,
freqs="M",
cosp=True,
)
sr.accuracy_bars()
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).