Duration returns#

This category group contains daily returns of the most liquid fixed-for-floating interest rate swaps of the currency area for various tenors. For most markets and periods, returns have been calculated based on interest rate swaps. However, non-deliverable swaps have been used for some parts of the history for emerging markets.

We approximate the excess returns of a duration contract of tenor \(m\) (in years) using the following equation:

(1)#\[\begin{equation} {XR}_{t,m} = y^{fix}_{t-1, m} - y_{t-1}^{float} - {DUR}_{t-1,m} (y_{t,m}^{fix} - y_{t-1,m}^{fix}) \end{equation}\]

where \(y_{t,m}^{fix}\) is the fixed rate, \(y_{t}^{float}\) is the underlying floating rate of the contract, and \({DUR}_{t,m}\) is the modified duration of the contract of tenor \(m\).

Duration return in % of notional#

Ticker: DU02YXR_NSA / DU05YXR_NSA / DU10YXR_NSA

Label: Duration return, in % of notional: 2-year maturity / 5-year maturity / 10-year maturity.

Definition: Return on fixed receiver position in main interest rate swaps contract traded in the currency area, % of notional of the contract, daily roll: 2-year maturity / 5-year maturity / 10-year maturity.

Notes:

  • The interest rate derivative for most currency areas is an interest rate swap. For some emerging market countries (China, India, South Korea, Thailand, Taiwan), non-deliverable swaps have been used.

  • The returns have been approximated as the sums of the yield spread between the fixed and floating leg and the daily change in yield times duration.

  • For the return formula, see the introduction above.

Vol-targeted duration return#

Ticker: DU02YXR_VT10 / DU05YXR_VT10 / DU10YXR_VT10

Label: Duration return for 10% vol target: 2-year maturity / 5-year maturity / 10-year maturity.

Definition: Return on fixed receiver position, % of risk capital on position scaled to 10% (annualized) volatility target, assuming monthly roll: 2-year maturity / 5-year maturity / 10-year maturity

Notes:

  • Positions are scaled to a 10% volatility target based on historic standard deviations for an exponential moving average with a half-life of 11 days. Positions are rebalanced at the end of each month with a constrained maximum leverage (notional to risk capital) of 20.

  • See further the above notes on “Duration return in % of notional” (DU02YXR_NSA / DU05YXR_NSA / DU10YXR_NSA).

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 = [
    "DU02YXR_NSA",
    "DU02YXR_VT10",
    "DU05YXR_NSA",
    "DU05YXR_VT10",
    "DU10YXR_NSA",
    "DU10YXR_VT10",
]
econ = []
mark = ["DU05YCRY_NSA", "DU05YCRY_VT10", "EQXR_NSA", "EQXR_V10"]  # 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,
        show_progress=True,
    )
    end = timer()

dfd = df

print("Download time from DQ: " + str(timedelta(seconds=end - start)))
Maximum number of tickers is 380
Downloading data from JPMaQS.
Timestamp UTC:  2023-09-06 13:13:30
Connection successful!
Number of expressions requested: 1520
Requesting data: 100%|█████████████████████████████████████████████████████████████████| 76/76 [00:24<00:00,  3.15it/s]
Downloading data: 100%|████████████████████████████████████████████████████████████████| 76/76 [00:40<00:00,  1.87it/s]
Download time from DQ: 0:01:22.430663

Availability#

cids_exp = sorted(
    list(set(cids) - set(cids_dmec + ["ARS", "PEN", "PHP", "TRY"]))
)  # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df:  []
Missing cids for DU02YXR_NSA:  ['RON', 'BRL']
Missing cids for DU02YXR_VT10:  ['RON', 'BRL']
Missing cids for DU05YXR_NSA:  ['RON', 'BRL']
Missing cids for DU05YXR_VT10:  ['RON', 'BRL']
Missing cids for DU10YXR_NSA:  ['RON', 'BRL']
Missing cids for DU10YXR_VT10:  ['RON', 'BRL']

Duration returns are available from 2000 for most developed market currencies and some emerging markets. Typically, most emerging market duration return series are available from the mid-2000s.

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/Duration returns_16_0.png
Last updated: 2023-09-06
xcatx = main
cidx = cids_exp

plot = msm.check_availability(
    dfd, xcats=xcatx, cids=cidx, start_size=(18, 4), start_years=False
)
../_images/Duration returns_17_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/Duration returns_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/Duration returns_19_0.png ../_images/Duration returns_19_1.png

History#

2-year duration returns#

Short-term duration returns have displayed very different variances across countries, as well as a pronounced proclivity towards outliers.

xcatx = ["DU02YXR_NSA"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start="2000-01-01",
    kind="box",
    title="Boxplots of 2-year duration returns since 2000",
    xcat_labels=["2-year duration returns"],
    size=(16, 8),
)
../_images/Duration returns_23_0.png

Most currency areas’ IRS fixed receiver positions have recorded positive cumulative returns over the past 20 years.

xcatx = ["DU02YXR_NSA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="Cumulative duration returns for receivers with 2-year fixed legs",
    title_adj=1.03,
    title_xadj=0.52,
    title_fontsize=27,
    legend_fontsize=17,
    cumsum=True,
    ncol=4,
    same_y=False,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
../_images/Duration returns_25_0.png

Correlation coefficients of short-term duration returns have been strongly positive across developed countries. Correlations have been much weaker across EM countries.

xcatx = "DU02YXR_NSA"
cidx = cids_exp

msp.correl_matrix(
    dfd,
    xcats=xcatx,
    cids=cidx,
    title="Cross-sectional correlations for 2-year duration returns, since 2000",
    size=(20, 14),
    cluster=True,
)
../_images/Duration returns_27_0.png

5-year and 10-year duration returns#

Differences in return variation have been substantial for longer durations and dwarf the respective mean returns.

xcatx = ["DU05YXR_NSA"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start="2000-01-01",
    kind="box",
    title="Boxplots of 5-year duration returns since 2000",
    size=(16, 8),
)
../_images/Duration returns_30_0.png
xcatx = ["DU10YXR_NSA"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start="2000-01-01",
    kind="box",
    title="Boxplots of 10-year duration returns since 2000",
    size=(16, 8),
)
../_images/Duration returns_31_0.png

Almost all countries posted positive long-term returns over the past 20 years. The exceptions were India, Russia and China.

xcatx = ["DU05YXR_NSA", "DU10YXR_NSA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="Cumulative duration returns for receivers with 5-year and 10-year fixed legs",
    xcat_labels=["5-year", "10-year"],
    title_adj=1.02,
    title_xadj=0.48,
    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/Duration returns_33_0.png

Vol-targeted duration returns#

Volatility targeting evens out differences in return variation across cross-sections, but cannot remove differences in historic kurtosis: some currency areas’ returns have posted fatter tails and, hence, larger outliers.

xcatx = ["DU02YXR_VT10"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start="2000-01-01",
    kind="box",
    title="Boxplots of 2-year duration returns, 10% vol-target, since 2000",
    xcat_labels=["2-year duration returns, 10% vol-target"],
    size=(16, 8),
)
../_images/Duration returns_36_0.png
xcatx = ["DU10YXR_VT10"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start="2000-01-01",
    kind="box",
    title="Boxplots of 10-year duration returns, 10% vol-target, since 2000",
    xcat_labels=["10-year duration returns, 10% vol-target"],
    size=(16, 8),
)
../_images/Duration returns_37_0.png

For most countries, long-term vol-targeted returns in long durations have outperformed those of shorter durations. This implies that Sharpe ratios of longer-duration positions have been superior over the past 20 years.

xcatx = ["DU02YXR_VT10", "DU10YXR_VT10"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="Cumulative duration returns for receivers with 2-year and 10-year fixed legs, 10% vol-target",
    xcat_labels=["2-year", "10-year"],
    cumsum=True,
    title_adj=1.03,
    title_xadj=0.47,
    label_adj=0.075,
    title_fontsize=27,
    legend_fontsize=17,
    ncol=4,
    same_y=False,
    size=(12, 7),
    aspect=1.7,
)
../_images/Duration returns_39_0.png

Importance#

Empirical Clues#

Developed markets have posted negative correlations between cumulative duration and equity returns, possibly due to the dominant influence of economic cycles (which push bond and equity prices in different directions).

xcatx = ["EQXR_NSA", "DU05YXR_NSA"]
cidx = cids_dm

cr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="M",
    lag=0,
    xcat_aggs=["sum", "sum"],
    start="2000-01-01",
    years=None,
)
EQXR_NSA misses: ['NOK', 'NZD'].
DU05YXR_NSA misses: ['DEM', 'ESP', 'FRF', 'ITL', 'NLG'].
cr.reg_scatter(
    title="Monthly cumulative duration and equity returns in developed countries since 2000",
    labels=False,
    coef_box="lower right",
    xlab="Monthly equity index future returns",
    ylab="Monthly 5-year duration (IRS) returns",
    reg_robust=True,
)
../_images/Duration returns_47_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).