Duration volatility risk premia and duration returns #
This notebook serves as an illustration of the points discussed in the post “Duration volatility risk premia” available on the Macrosynergy website.
The post argues, that simple duration VRP (volatility risk premia) can help predict idiosyncratic IRS returns in nonUSD markets. The post also argues that two derived concepts of volatility risk premia hold can help create better signals for fixedincome positions. The first one is term spreads, which is the differences between volatility risk premia for longermaturity and shortermaturity IRS contracts. The second one is maturity spreads, which are the differences between volatility risk premia of longer and shortermaturity options, as indicative of a fear of risk escalation, which affects mainly fixed receivers. Indeed, maturity spreads have been positively and significantly related to subsequent fixedrate receiver returns. These premia are best combined with fundamental indicators of the related risks to give valid signals for fixedincome positions.
This notebook provides the essential code required to replicate the analysis discussed in the post.
The notebook covers the three main parts:

Get Packages and JPMaQS Data: This section is responsible for installing and importing the necessary Python packages that are used throughout the analysis. It checks data availability, and blacklist periods (if any).

Transformations and Checks: In this part, the notebook performs various calculations and transformations on the data to derive the relevant signals and targets used for the analysis, including rolling medians, means, averages, zscores, maturity spreads and other metrics or ratios used in the analysis.

Value Checks: This is the most critical section, where the notebook calculates and implements the trading strategies based on the hypotheses tested in the post. Depending on the analysis, this section involves backtesting various trading strategies targeting outright or relative returns. The strategies utilize the indicators derived in the previous section.
It’s important to note that while the notebook covers a selection of indicators and strategies used for the post’s main findings, there are countless other possible indicators and approaches that can be explored by users, as mentioned in the post. Users can modify the code to test different hypotheses and strategies based on their own research and ideas. Best of luck with your research!
Get packages and JPMaQS data #
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os
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
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
DB(JPMAQS,<cross_section>_<category>,<info>)
, where
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 realtime 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. For more information see
here
.
Largely, only the standard packages in the Python data science stack are required to run this notebook. The specialized
macrosynergy
package
is also needed to download JPMaQS data and for quick analysis of quantamental data and value propositions.
The description of each JPMaQS category is available under Macro quantamental academy . For tickers used in this notebook see Duration volatility risk premia , Duration returns , and Equity index future returns .
cids_90 = ["EUR", "GBP", "USD", "SEK"]
cids_00 = ["HKD", "HUF", "ILS", "NOK", "PLN", "ZAR"]
cids_10 = ["CHF", "JPY", "KRW"]
cids_vrp = cids_90 + cids_00 + cids_10
cids_vrxu = list(set(cids_vrp)  set(["USD"]))
main = [
"IRVRP03M02Y_NSA",
"IRVRP06M02Y_NSA",
"IRVRP01Y02Y_NSA",
"IRVRP03M03Y_NSA",
"IRVRP06M03Y_NSA",
"IRVRP01Y03Y_NSA",
"IRVRP03M05Y_NSA",
"IRVRP06M05Y_NSA",
"IRVRP01Y05Y_NSA",
]
xtra = ["DU02YXRxEASD_NSA", "DU05YXRxEASD_NSA", "EQXR_NSA"]
rets = ["DU02YXR_NSA", "DU05YXR_NSA", "DU02YXR_VT10", "DU05YXR_VT10"]
xcats = main + xtra + rets
# Resultant tickers
tickers = [cid + "_" + xcat for cid in cids_vrp for xcat in xcats]
print(f"Maximum number of tickers is {len(tickers)}")
Maximum number of tickers is 208
# Download series from J.P. Morgan DataQuery by tickers
start_date = "19900101"
end_date = "20230501"
# Retrieve credentials
client_id: str = os.getenv("DQ_CLIENT_ID")
client_secret: str = os.getenv("DQ_CLIENT_SECRET")
with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
df = dq.download(
tickers=tickers,
start_date=start_date,
# end_date=end_date,
suppress_warning=True,
metrics=["all"],
show_progress=True,
)
Downloading data from JPMaQS.
Timestamp UTC: 20240321 13:10:34
Connection successful!
Requesting data: 100%██████████ 42/42 [00:08<00:00, 4.95it/s]
Downloading data: 100%██████████ 42/42 [00:31<00:00, 1.32it/s]
Some expressions are missing from the downloaded data. Check logger output for complete list.
120 out of 832 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 8931 dates are missing.
scols = ["cid", "xcat", "real_date", "value"] # required columns
dfx = df[scols].copy()
dfx.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1100106 entries, 0 to 1100105
Data columns (total 4 columns):
# Column NonNull Count Dtype
   
0 cid 1100106 nonnull object
1 xcat 1100106 nonnull object
2 real_date 1100106 nonnull datetime64[ns]
3 value 1100106 nonnull float64
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 33.6+ MB
Availability #
It is important to assess data availability before conducting any analysis. It allows identifying any potential gaps or limitations in the dataset, which can impact the validity and reliability of analysis and ensure that a sufficient number of observations for each selected category and crosssection is available as well as determining the appropriate time periods for analysis.
msm.missing_in_df(df, xcats=xcats, cids=cids_vrp)
Missing xcats across df: []
Missing cids for DU02YXR_NSA: []
Missing cids for DU02YXR_VT10: []
Missing cids for DU02YXRxEASD_NSA: []
Missing cids for DU05YXR_NSA: []
Missing cids for DU05YXR_VT10: []
Missing cids for DU05YXRxEASD_NSA: []
Missing cids for EQXR_NSA: ['NOK', 'HUF', 'ILS']
Missing cids for IRVRP01Y02Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP01Y03Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP01Y05Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP03M02Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP03M03Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP03M05Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP06M02Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP06M03Y_NSA: ['HKD', 'HUF', 'KRW']
Missing cids for IRVRP06M05Y_NSA: ['HKD', 'HUF', 'KRW']
msm.check_availability(
df,
xcats=xcats,
cids=cids_vrp,
)
Blacklist dictionary #
ZAR data was not updated March 2019 to July 2020, due to the lack of trader at J.P. Morgan, hence we create a blacklist dictionary and pass it to several package functions in this notebook that exclude the blacklisted period from related analyses.
zar_black = ["20190315", "20200731"]
dv_black = {"ZAR": [pd.to_datetime(s) for s in zar_black]}
dv_black
{'ZAR': [Timestamp('20190315 00:00:00'), Timestamp('20200731 00:00:00')]}
Transformations and checks #
Features #
Volatility return premia #
As the first step, part of preliminary analysis, we display volatility risk premia for 2year and 5year IRS receivers with swaption maturity of 3 months on the timeline as well as separately their means and standard deviations. Please see here for the definition of the indicators and Introduction to Macrosynergy package for the standard functions used throughout this notebook.
Shorterduration VRP have been highest in economies with low shortterm interest rates (which have little realized rates volatility).
xcats_sel = ["IRVRP03M02Y_NSA", "IRVRP03M05Y_NSA"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title=None,
ylab=None,
start="20000101",
)
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_vrp,
ncol=3,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title=None,
xcat_labels=None,
)
For most countries for 2year duration, the VRP based on shortermaturity options is larger than the VRP based on longermaturity options (5 years). High current volatility is often expected to revert to its longterm average or decline over time, which can impact the relationship between longterm implied volatility and recently realized volatility. This expectation of mean reversion in volatility can influence the estimation of volatility risk premia based on longerdated maturities. Given this understanding, incorporating both short and longterm lookbacks in estimating the expected realized measure can help provide a more accurate estimation of volatility risk premia for longerdated maturities.
xcats_sel = ["IRVRP03M02Y_NSA", "IRVRP01Y02Y_NSA"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title="Volatility risk premia for 2year IRS receivers with swaption maturity of 3 months and 1 year",
ylab=None,
start="20000101",
)
For longerduration IRS (5 years) the gap between shortterm and longerterm maturitybased VRPs has been smaller.
xcats_sel = ["IRVRP03M05Y_NSA", "IRVRP01Y05Y_NSA"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title="Volatility risk premia for 5year IRS receivers with swaption maturity of 3 months and 1 year",
ylab=None,
start="20000101",
)
Rolling means and medians #
Adding a rolling average to the estimation of volatility premia can indeed introduce stability to the estimates, but it’s important to note that it also introduces a time lag in capturing shifts in the premium. The code below creates a rolling 5day average for all volatility premia categories, appending a postfix
_5DMA
to indicate the modified category.
calcs = []
for vrp in main:
calc = [f"{vrp}_5DMA = {vrp}.rolling(5).mean() "]
calcs += calc
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
For comparison, the new rolling 5day moving average is plotted against the original series.
xcats_sel = ["IRVRP03M02Y_NSA", "IRVRP03M02Y_NSA_5DMA"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=["USD"],
start="20200101",
title=None,
)
VRP averages #
Simple averages of duration VRPs may be most representative of the general concept and reduce the effects of individual pricing errors. The average volatility risk premium here is based on the arithmetic average of the premium for three underlying IRS tenors (2, 3, and 5 years) and three option maturities (3, 6, and 12 months).
The cell below creates 2 types of averages for each crosssection:
IRVRP_AVG
 represents the average of original volatility premia
IRVRP_5DMA_AVG
represents the average of rolling 5 days averages
sum_str = " + ".join((vrp for vrp in main)) # join list to string of sum
sum_str_5dma = " + ".join((vrp + "_5DMA" for vrp in main))
calc1 = f"IRVRP_AVG = ( {sum_str} ) / {len(main)}"
calc2 = f"IRVRP_5DMA_AVG = ( {sum_str_5dma} ) / {len(main)}"
calcs = [calc1] + [calc2]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
Volatility risk premia across the available sample periods have on average been positive for all countries. The highest premia were charged in Switzerland and Israel, the lowest in the U.S. and South Korea. In all countries, the premia have at least temporarily been negative.
xcats_sel = ["IRVRP_AVG"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title="Volatility risk premia across available sample periods: averages across maturities and tenors",
ylab=None,
start="19920101",
)
The premia have been stationarity with sustained periods of months or years above average and short periods below average or in negative territory. Beyond, there has been ample shortterm volatility, even after taking 5day moving averages.
xcats_sel = ["IRVRP_5DMA_AVG"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_vrxu,
ncol=3,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="NonUSD: Average volatility risk premium across durations and maturities, 5day rolling",
xcat_labels=None,
)
U.S. data are now available for three decades and reveal pronounced and sustained phases of negative and positive premia.
xcats_sel = ["IRVRP_5DMA_AVG"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=["USD"],
start="19920101",
title="USD: Average volatility risk premium across durations and maturities, 5day rolling",
xcat_labels=["based on USD swaption and IRS markets"],
)
Volatility risk premia have been positively correlated across all markets, based on the longest common samples. This suggests that they reflect common global factors.
msp.correl_matrix(
dfx,
xcats="IRVRP_5DMA_AVG",
cids=cids_vrp,
start="20000101",
cluster=True,
title="Crosssectional correlation coefficients of volatility risk premia",
)
VRP term spreads #
Here term spread refers to the difference between 5year and 2year IRS duration VRPs for the same option maturity. Conceptually, this refers to the difference in premia for bearing longterm uncertainty versus shortterm uncertainty.
The effective spread serves as an indicator of the dominance of either structural uncertainty (resulting in a positive spread) or cyclical uncertainty (resulting in a negative spread). Structural uncertainty refers to factors related to the overall economic or financial structure, such as longterm economic trends, systemic risks, or policy uncertainties. Cyclical uncertainty, on the other hand, relates to shortterm economic fluctuations, business cycles, or market sentiment.
The next cell calculates term spreads and creates a new category by appending a postfix
_TS
to the original category.
omats = ["03M", "06M", "01Y"]
calcs = []
for omat in omats: # term spreads across option maturities
calc1 = [f"IRVRP_TS{omat} = IRVRP{omat}05Y_NSA  IRVRP{omat}02Y_NSA"]
calc2 = [f"IRVRP_TS{omat}_5DMA = IRVRP{omat}05Y_NSA_5DMA  IRVRP{omat}02Y_NSA_5DMA"]
calcs += calc1 + calc2
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
It is useful to calculate an average over the three option maturities. The new indicator gets the postfix
_AVG
in addition to
_TS
sum_str = " + ".join((f"IRVRP_TS{omat}" for omat in omats))
sum_str_5dma = " + ".join((f"IRVRP_TS{omat}_5DMA" for omat in omats))
calc1 = f"IRVRP_TS_AVG = ( {sum_str} ) / {len(omats)}"
calc2 = f"IRVRP_TS_5DMA_AVG = ( {sum_str_5dma} ) / {len(omats)}"
calcs = [calc1] + [calc2]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
Historically, the term spread has been mostly negative, i.e. premia for taking shortduration volatility risk have been higher than for taking longduration volatility risk. This may be an indication that interest rates in most countries are seen as more anchored in the long run than in the short run.
xcats_sel = ["IRVRP_TS_5DMA_AVG"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title="Means and standard deviations of term spreads of volatility risk premia",
ylab="difference of premium for 5year and 2year interest rate swaps",
start="20000101",
)
Over time, the spreads have displayed trends, cycles, and ample shortterm volatility. Here we display the timeline of US term spreads of volatility risk premia, 5 day rolling mean (US has the longest history of data, available singe 1992)
xcats_sel = ["IRVRP_TS_5DMA_AVG"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=["USD"],
start="19920101",
title_adj=1.01,
title="USD: Term spreads of volatility risk premia, 5day rolling mean",
xcat_labels=["based on USD swaption and IRS markets"],
)
Correlations of term spreads across economies have been mixed. In particular, USD term spread correlation with other countries has been mostly negative. However, the correlation of term spreads for the European countries has been mostly positive.
msp.correl_matrix(
dfx,
xcats="IRVRP_TS_5DMA_AVG",
cids=cids_vrp,
start="20000101",
cluster=True,
title="Crosssectional correlation coefficients of term spreads of volatility risk premia since 2000",
)
VRP maturity spreads #
Directional #
Here maturity spread means the difference between the VRP based on a longermaturity option compared to a shortermaturity option, for the same underlying duration. Since realized volatility for both is estimated in the same day this is equivalent to the scaled difference between implied vols of the 1year and 3month options for the same maturity. This may give an indication if volatility is expected to be persistent (positive spread) or shortlived (negative spread).
durs = ["02Y", "03Y", "05Y"]
calcs = []
for dur in durs: # term spreads across option maturities
calc1 = [f"IRVRP_MS{dur} = IRVRP01Y{dur}_NSA  IRVRP03M{dur}_NSA"]
calc2 = [f"IRVRP_MS{dur}_5DMA = IRVRP01Y{dur}_NSA_5DMA  IRVRP03M{dur}_NSA_5DMA"]
calcs += calc1 + calc2
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
An average maturity spread is simply the mean overall underlying durations.
sum_str = " + ".join((f"IRVRP_MS{dur}" for dur in durs))
sum_str_5dma = " + ".join((f"IRVRP_MS{dur}_5DMA" for dur in durs))
calc1 = f"IRVRP_MS_AVG = ( {sum_str} ) / {len(durs)}"
calc2 = f"IRVRP_MS_5DMA_AVG = ( {sum_str_5dma} ) / {len(durs)}"
calcs = [calc1] + [calc2]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
Maturity spreads have been mostly negative and displayed very different standard deviations across currency areas with larger countries posting smaller fluctuations.
xcats_sel = ["IRVRP_MS_5DMA_AVG"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title="Means and standard deviations of maturity spreads of volatility risk premia",
ylab="difference of premium for 1year and 3month swaption maturity",
start="20000101",
)
The time series of maturity spreads have been stationary with pronounced cycles around a negative mean, as exemplified by the U.S. history in the graph below:
xcats_sel = ["IRVRP_MS_5DMA_AVG"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=["USD"],
start="19920101",
title="USD: Maturity spreads of volatility risk premia, 5day rolling mean",
xcat_labels=["based on USD swaption and IRS markets"],
)
The shortduration maturity spreads turned out to be the most negative. JPY, HKD and CHF have been the countries with the deepest negative maturity spreads for 2year durations.
xcats_sel = ["IRVRP_MS02Y", "IRVRP_MS05Y"]
msp.view_ranges(
dfx,
cids=cids_vrp,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title=None,
ylab=None,
start="20000101",
)
Correlations across currency areas are mixed. Most post positive correlation with either the U.S. or the Euro area.
msp.correl_matrix(
dfx,
xcats="IRVRP_MS_5DMA_AVG",
cids=cids_vrp,
start="20000101",
cluster=True,
title="Crosssectional correlation coefficients of maturity spreads since 2000",
)
Targets #
Outright returns #
Outright returns and voltargeted returns across currency areas have shown similar cyclical patterns.
xcats_sel = ["DU02YXR_NSA", "DU05YXR_NSA", "DU02YXR_VT10", "DU05YXR_VT10"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_vrp,
ncol=4,
cumsum=True,
start="20000101",
same_y=True,
size=(12, 12),
all_xticks=True,
title="Outright returns and voltargeted returns",
xcat_labels=None,
title_fontsize=28
)
Relative volparity returns (across durations and versus USD) #
This relative return is the 5year IRS receiver return minus the 2year IRS receiver return, both based on voltargeted positions.
calc1 = "DU5v2YXR = DU05YXR_VT10  DU02YXR_VT10"
calc2 = "DU02YvUSDXR = DU02YXR_VT10  iUSD_DU02YXR_VT10"
calc3 = "DU05YvUSDXR = DU05YXR_VT10  iUSD_DU05YXR_VT10"
calcs = [calc1, calc2, calc3]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_vrp, blacklist=dv_black)
dfx = msm.update_df(dfx, dfa)
While cumulative returns of curve trades have posted local trends and cycles, only CHF, JPY and NOK posted longerterm drifts
xcats_sel = ["DU5v2YXR"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_vrp,
ncol=4,
cumsum=True,
start="20000101",
same_y=True,
size=(12, 12),
all_xticks=True,
title=None,
xcat_labels=None,
)
Curve trade returns have generally been positively correlated across currency areas.
msp.correl_matrix(
dfx, xcats="DU5v2YXR", cids=cids_vrp, start="20000101", cluster=True
)
Most currency areas’ receiver positions have outperformed the USD IRS receivers over the long term.
xcats_sel = ["DU02YvUSDXR", "DU05YvUSDXR"]
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_vrxu,
ncol=4,
cumsum=True,
start="20000101",
same_y=True,
size=(12, 12),
all_xticks=True,
title=None,
xcat_labels=None,
)
Value checks #
VRP and directional returns #
There has been no clear relation between duration VRP and subsequent IRS returns. Higher premia on volatility risk have not translated into higher returns on outright duration exposure. The correlation coefficient is around 0. Using “map” as prob_est instead of the default “pool” diminishes the significance probability further. For details on the test please see Testing macro trading factors . Since we see almost no correlation between VRP and directional returns, we will not construct a trading strategy based on simple VRP, instead, we proceed with the investigation of the two derived concepts, term and maturity spreads, on subsequent returns.
xcats_vpa_2vt = ["IRVRP_5DMA_AVG", "DU02YXR_VT10"]
xcats_sel = xcats_vpa_2vt
cids_sel = cids_vrp
cr = msp.CategoryRelations(
dfx,
xcats=xcats_sel,
cids=cids_sel,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
start="20000101",
xcat_trims=[None, None],
)
cr.reg_scatter(
labels=False,
prob_est="map",
coef_box="lower right",
title="Volatility risk premium and subsequent weekly IRS returns across all markets since 2000",
xlab="Average volatility risk premium across durations and option maturities",
ylab="Next month's 2year IRS receiver return",
)
IRVRP_5DMA_AVG misses: ['HKD', 'HUF', 'KRW'].
VRP and relative returns #
The correlation of volatility risk premia in nonU.S. markets with subsequent IRS receiver returns relative to the U.S. has been positive with modest significance. This suggests that premia are charged for idiosyncratic volatility risk and predictive for idiosyncratic returns in nonUSD currency areas. However, if the test is run using timespecific random effects estimation method, the significance goes down further.
xcats_vpa_5vu = ["IRVRP_5DMA_AVG", "DU05YvUSDXR"]
xcats_sel = xcats_vpa_5vu
cids_sel = cids_vrxu
cr = msp.CategoryRelations(
dfx,
xcats=xcats_sel,
cids=cids_sel,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
start="20000101",
xcat_trims=[None, None],
blacklist=dv_black,
)
cr.reg_scatter(
labels=False,
coef_box="lower left",
title="Volatility risk premia and subsequent relative IRS returns across all markets since 2000",
xlab="Average volatility risk premium across durations and option maturities",
ylab="Next month's 5year IRS receiver return relative to USD receiver return (volparity)",
)
IRVRP_5DMA_AVG misses: ['HKD', 'HUF', 'KRW'].
cr.reg_scatter(
labels=False,
coef_box="lower left",
title=None,
xlab=None,
ylab=None,
separator="cids",
)
We use
SignalReturnRelations()
function from the
Macrosynergy package
, which analyses and compares the relationships between the chosen signals and the panel of subsequent returns. There is no regression analysis involved, rather the sign of the signal is used for predicting the sign of the target.
xcats_sel = xcats_vpa_5vu
cids_sel = cids_vrxu
srr = mss.SignalReturnRelations(
dfx,
cids=cids_sel,
sigs=xcats_sel[0],
rets=xcats_sel[1],
freqs="W",
start="20000101",
blacklist=dv_black,
)
display(srr.summary_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

Panel  0.514  0.507  0.751  0.517  0.521  0.494  0.001  0.913  0.006  0.362  0.506 
Mean years  0.515  0.508  0.758  0.516  0.519  0.497  0.000  0.363  0.007  0.372  0.505 
Positive ratio  0.720  0.640  0.960  0.640  0.640  0.440  0.480  0.320  0.480  0.360  0.640 
Mean cids  0.514  0.506  0.755  0.517  0.521  0.492  0.004  0.569  0.010  0.486  0.506 
Positive ratio  0.889  0.667  1.000  1.000  1.000  0.333  0.667  0.333  0.667  0.333  0.667 
xcats_sel = xcats_vpa_5vu
cids_sel = cids_vrxu
start_date = "20000101"
sigs = [xcats_sel[0]]
naive_pnl = msn.NaivePnL(
dfx,
ret=xcats_sel[1],
sigs=sigs,
cids=cids_sel,
start=start_date,
blacklist=dv_black,
)
for sig in sigs:
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="zero",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZ0",
)
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="mean",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZM",
)
naive_pnl.make_long_pnl(vol_scale=10, label="Long")
pnls = [xcats_sel[0] + "PZ0", "Long"]
dict_labels = {"IRVRP_5DMA_AVGPZ0": "Relative 5year IRS receiver positions based on local volatility risk premia", "Long": "Continuously long relative IRS receiver positions (volparity)"}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start_date,
title="PnLs of global relative IRS positions (local currency versus USD)",
xcat_labels=dict_labels,
)
Term spreads and relative returns #
There has been a positive correlation but due largely to size comovement rather than high accuracy of directional predictions. Value generation would have been impressive, though, based on a standard generic strategy.
xcats_tsa_52 = ["IRVRP_TS_5DMA_AVG", "DU5v2YXR"]
xcats_sel = xcats_tsa_52
cids_sel = cids_vrp
cr = msp.CategoryRelations(
dfx,
xcats=xcats_sel,
cids=cids_sel,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
start="20000101",
xcat_trims=[None, None],
)
cr.reg_scatter(
labels=False,
coef_box="lower left",
prob_est="map",
title="Term spreads of volatility risk premia and subsequent 5year versus 2year IRS returns",
xlab="Term spread",
ylab="Next month's 5year versus 2year IRS returns (volparity)",
)
IRVRP_TS_5DMA_AVG misses: ['HKD', 'HUF', 'KRW'].
xcats_sel = xcats_tsa_52
cids_sel = cids_vrp
srr = mss.SignalReturnRelations(
dfx,
cids=cids_sel,
sigs=xcats_sel[0],
rets=xcats_sel[1],
freqs="W",
start="20000101",
)
display(srr.summary_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

Panel  0.506  0.504  0.532  0.527  0.531  0.477  0.038  0.000  0.006  0.376  0.504 
Mean years  0.506  0.504  0.549  0.526  0.528  0.479  0.026  0.375  0.008  0.418  0.504 
Positive ratio  0.520  0.520  0.560  0.640  0.640  0.240  0.680  0.520  0.560  0.400  0.520 
Mean cids  0.500  0.502  0.486  0.530  0.526  0.478  0.037  0.372  0.011  0.563  0.503 
Positive ratio  0.600  0.500  0.500  1.000  0.900  0.300  0.800  0.700  0.600  0.300  0.500 
xcats_sel = xcats_tsa_52
cids_sel = cids_vrp
start_date = "20000101"
sigs = [xcats_sel[0]]
naive_pnl = msn.NaivePnL(
dfx,
ret=xcats_sel[1],
sigs=sigs,
cids=cids_sel,
start=start_date,
blacklist=dv_black,
)
for sig in sigs:
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="zero",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZ0",
)
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="mean",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZM",
)
naive_pnl.make_long_pnl(vol_scale=10, label="Long")
pnls = [xcats_sel[0] + "PZ0", "Long"]
start_date = "20000101"
dict_labels = {"IRVRP_TS_5DMA_AVGPZ0": "Relative IRS receiver positions based on term spreads volatility risk premia",
"Long": "Continuously long 5years versus 2years receiver positions"}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start_date,
title="PnLs of global IRS positions (5years versus 2years, risk parity)",
xcat_labels=dict_labels,
)
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start_date,
)
display(df_eval)
xcat  IRVRP_TS_5DMA_AVGPZ0  Long 

Return (pct ar)  4.280303  2.038829 
St. Dev. (pct ar)  10.0  10.0 
Sharpe Ratio  0.42803  0.203883 
Sortino Ratio  0.663843  0.277556 
Max 21day draw  11.591743  23.407636 
Max 6month draw  28.352643  33.419888 
Traded Months  291  291 
Maturity spreads and directional returns #
There has been a mild positive correlation between maturity spreads and subsequent duration returns. The drawback as a trading signal is mainly the almost 70% short bias. Part of the short bias relates to the unexploited mean reversion of realized duration volatility.
xcats_msa_2vt = ["IRVRP_MS_5DMA_AVG", "DU05YXR_VT10"]
xcats_sel = xcats_msa_2vt
cids_sel = cids_vrp
cr = msp.CategoryRelations(
dfx,
xcats=xcats_sel,
cids=cids_sel,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
start="20000101",
xcat_trims=[None, None],
)
cr.reg_scatter(
labels=False,
coef_box="lower left",
title="Maturity spreads and subsequent IRS returns across all markets since 2000",
xlab="Average maturity spreads across underlying IRS tenors",
ylab="Next month's 5year IRS receiver return (voltargeted at 10%)",
)
IRVRP_MS_5DMA_AVG misses: ['HKD', 'HUF', 'KRW'].
xcats_sel = xcats_msa_2vt
cids_sel = cids_vrp
srr = mss.SignalReturnRelations(
dfx,
cids=cids_sel,
sigs=xcats_sel[0],
rets=xcats_sel[1],
freqs="W",
start="20000101",
)
display(srr.summary_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

Panel  0.491  0.506  0.321  0.540  0.549  0.464  0.029  0.004  0.013  0.041  0.506 
Mean years  0.496  0.504  0.325  0.542  0.546  0.462  0.013  0.377  0.005  0.423  0.503 
Positive ratio  0.400  0.480  0.160  0.760  0.640  0.200  0.480  0.320  0.520  0.240  0.480 
Mean cids  0.488  0.496  0.312  0.540  0.529  0.464  0.011  0.413  0.006  0.409  0.503 
Positive ratio  0.400  0.500  0.100  1.000  0.900  0.000  0.700  0.500  0.700  0.500  0.500 
A simple strategy that uses only the maturity spread (zscore around zero) as a signal for receiver versus payer position would not have created much positive PnL over the past 22 years. This is due mainly to its strong shortduration bias. Since, in stable monetary regimes, there is not much “escalation risk premium” on offer, the signal would have been negative in almost 70% of all weeks across all countries. The signal would have implied a massive short duration risk bias.
xcats_sel = xcats_msa_2vt
cids_sel = cids_vrp
start_date = "20000101"
sigs = [xcats_sel[0]]
naive_pnl = msn.NaivePnL(
dfx,
ret=xcats_sel[1],
sigs=sigs,
cids=cids_sel,
start=start_date,
)
for sig in sigs:
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="zero",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZ0",
)
naive_pnl.make_pnl(
sig,
sig_op="zn_score_pan",
neutral="mean",
thresh=2,
rebal_freq="weekly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "PZM",
)
naive_pnl.make_long_pnl(vol_scale=10, label="Long")
The chart illustrates that positive correlation and predictive power alone are not enough to make a good directional positioning signal. The signal must also set the right longterm bias and gather a critical mass of explanatory power for all the premia charged on a contract. The maturity spread, which reflects a single type of risk premium, cannot provide that. As with many short riskbias strategies, its own overall performance as a trading signal is not impressive. However, it is a valid contributor to signal for directional exposure to duration risk.
pnls = [xcats_sel[0] + "PZ0", "Long"]
dict_labels = {"IRVRP_MS_5DMA_AVGPZ0": "based on maturity spreads only",
"Long": "constant long receivers only"}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start_date,
title="PnLs of global IRS positions (5years tenor, targeted at 10% vol)",
xcat_labels=dict_labels,
)
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start_date,
)
display(df_eval)
xcat  IRVRP_MS_5DMA_AVGPZ0  Long 

Return (pct ar)  0.787395  3.97491 
St. Dev. (pct ar)  10.0  10.0 
Sharpe Ratio  0.078739  0.397491 
Sortino Ratio  0.115315  0.55272 
Max 21day draw  20.419992  19.439062 
Max 6month draw  35.751574  39.144904 
Traded Months  291  291 
xcats_sel = xcats_tsa_52
cids_sel = ["USD"]
srr = mss.SignalReturnRelations(
dfx,
cids=cids_sel,
sigs=xcats_sel[0],
rets=xcats_sel[1],
freqs="W",
start="19920101",
)
display(srr.summary_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

Panel  0.531  0.527  0.709  0.520  0.536  0.518  0.069  0.005  0.029  0.074  0.522 
Mean years  0.529  0.542  0.718  0.520  0.530  0.506  0.029  0.520  0.018  0.587  0.513 
Positive ratio  0.576  0.576  0.697  0.576  0.667  0.455  0.606  0.333  0.576  0.182  0.545 
Mean cids  0.531  0.527  0.709  0.520  0.536  0.518  0.069  0.005  0.029  0.074  0.522 
Positive ratio  1.000  1.000  1.000  1.000  1.000  1.000  1.000  1.000  1.000  1.000  1.000 