Duration carry #
This category contains daily carry of the dominant liquid fixed-for-floating swaps of the currency area for various tenors. Carry has two components: (a) the yield to maturity in excess of the financing cost (“term premium”) and (b) the “roll-down” of the bond across the yield curve as time to maturity is shortening. Unlike for other asset types there is no distinction between real and nominal carry because both the long position and the funding lag are in nominal terms.
We approximate carry of a duration contract of tenor \(m\) (in years) using the following 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 carry in % of notional #
Ticker : DU02YCRY_NSA / DU05YCRY_NSA / DU10YCRY_NSA
Label : Duration carry, in % of notional: 2-year maturity / 5-year maturity / 10-year maturity.
Definition : Carry on a fixed receiver position in the 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 :
-
Duration carry has two components: the term premium and the roll-down of the bond across the yield curve per time.
-
The interest rate derivative for most currency areas is an interest rate swap. For some emerging market countries (China, India, Korea, Thailand, Taiwan) non-deliverable swaps have been used.
-
For the carry formula, see Appendix 1 .
Vol-targeted duration carry #
Ticker : DU02YCRY_VT10 / DU05YCRY_VT10 / DU10YCRY_VT10
Label : Duration carry for 10% vol target: 2-year maturity / 5-year maturity / 10-year maturity.
Definition : Carry on fixed receiver position, % of risk capital on position scaled to 10% annualized volatility target, assuming daily roll: 2-year maturity / 5-year maturity / 10-year maturity
Notes :
-
Duration carry has two components: the term premium and the roll-down of the bond across the yield curve per time.
-
The interest rate derivative for most currency areas is an interest rate swap. For some emerging market countries (China, India, Korea, Thailand, TWD), non-deliverable swaps have been used.
-
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. Leverage (ratio of contract notional to risk capital) is subject to a maximum of 20.
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 = [
"DU02YCRY_NSA",
"DU02YCRY_VT10",
"DU05YCRY_NSA",
"DU05YCRY_VT10",
"DU10YCRY_NSA",
"DU10YCRY_VT10",
]
econ = [
"RYLDIRS05Y_NSA",
"BXBGDPRATIO_NSA_12MMA",
"PCREDITGDP_SJA_D1M1ML12",
] # economic context
mark = [
"DU02YXR_NSA",
"DU02YXR_VT10",
"DU05YXR_NSA",
"DU05YXR_VT10",
"DU10YXR_NSA",
"DU10YXR_VT10",
] # 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 570
Downloading data from JPMaQS.
Timestamp UTC: 2023-06-14 10:31:53
Connection successful!
Number of expressions requested: 2280
Requesting data: 100%|███████████████████████████████████████████████████████████████| 114/114 [00:34<00:00, 3.26it/s]
Downloading data: 100%|██████████████████████████████████████████████████████████████| 114/114 [01:08<00:00, 1.66it/s]
Download time from DQ: 0:02:09.018754
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: set()
Missing cids for DU02YCRY_NSA: {'RON', 'BRL'}
Missing cids for DU02YCRY_VT10: {'RON', 'BRL'}
Missing cids for DU05YCRY_NSA: {'RON', 'BRL'}
Missing cids for DU05YCRY_VT10: {'RON', 'BRL'}
Missing cids for DU10YCRY_NSA: {'RON', 'BRL'}
Missing cids for DU10YCRY_VT10: {'RON', 'BRL'}
Quantamental indicators of duration carry are available from the early 2000s for most developed market currencies and some emerging markets. Typically, most emerging market duration carry 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 2 .
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())
Last updated: 2023-06-14
xcatx = main
cidx = cids_exp
plot = msm.check_availability(
dfd, xcats=xcatx, cids=cidx, start_size=(18, 4), start_years=False
)
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",
)
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 #
2-year duration carry #
Short-term carry has predominantly been positive but the standard deviations have been large relative to the absolute averages.
xcatx = ["DU02YCRY_NSA"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="mean",
start="2000-01-01",
title="Means and standard deviations of 2-year duration carries since 2000",
kind="bar",
size=(16, 8),
)
xcatx = ["DU02YCRY_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start="2000-01-01",
title="Duration carry for receivers with 2-year fixed legs",
title_adj=1.02,
title_xadj=0.5,
title_fontsize=27,
legend_fontsize=17,
cumsum=False,
ncol=4,
same_y=False,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
For most countries, cross-sectional correlations of short-term carry have been positive over the last 20 years. However China, Hungary, Indonesia and Russia have displayed pronounced idiosyncratic fluctuations.
xcatx = "DU02YCRY_NSA"
cidx = cids_exp
msp.correl_matrix(
dfd,
xcats=xcatx,
cids=cidx,
title="Cross-sectional correlations for 2-year duration carries, since 2000",
size=(20, 14),
)
5 and 10-year duration carry #
Standard deviations for longer-term carry have typically exceeded absolute carry averages, particularly for higher-yield emerging markets.
xcatx = ["DU05YCRY_NSA", "DU10YCRY_NSA"]
cidx = cids_exp
msp.view_ranges(
dfd,
xcats=xcatx,
cids=cidx,
sort_cids_by="mean",
start="2000-01-01",
title="Means and standard deviations of 5-year and 10-year duration carries since 2000",
xcat_labels=["5-year", "10-year"],
kind="bar",
size=(16, 8),
)
xcatx = ["DU05YCRY_NSA", "DU10YCRY_NSA"]
cidx = cids_exp
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start="2000-01-01",
title="Duration carry for receivers with 5-year and 10-year fixed legs",
title_adj=1.02,
title_xadj=0.45,
title_fontsize=27,
legend_fontsize=17,
xcat_labels=["5-year", "10-year"],
cumsum=False,
ncol=4,
same_y=True,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
Vol-targeted duration carry #
Duration carry for 10% annualized volatility targets has been predominantly positive and ranged from a negative 50 basis points to a positive 100 basis points.
xcatx = ["DU02YCRY_VT10", "DU05YCRY_VT10"]
msp.view_timelines(
dfd,
xcats=xcatx,
cids=cidx,
start="2000-01-01",
title="Duration carry for receivers with 2-year and 5-year fixed legs, 10% vol-target",
title_adj=1.02,
title_fontsize=27,
legend_fontsize=17,
xcat_labels=["2-year", "5-year"],
cumsum=False,
ncol=4,
same_y=False,
size=(12, 7),
aspect=1.7,
all_xticks=True,
)
Importance #
Research Links #
“Empirical evidence for 27 markets suggests that carry on interest rate swaps has been positively correlated with subsequent returns for the past two decades. Indeed, a naïve strategy following carry as signal has produced respectable risk-adjusted returns. However, this positive past performance masks the fundamental flaw of the carry signal: it disregards the expected future drift in interest rates and favours receiver position in markets with very low real rates. In the 2000s and 2010s this oversight mattered little because inflation and yields drifted broadly lower. If the inflation cycle turns or just stabilizes, however, short-term rates normalization should become very consequential. Indeed, enhancing the IRS carry signal by a plausible medium term drift in short rates has already in the past produced more stable returns and more convincing actual ‘alpha’.” Macrosynergy
Empirical Clues #
On average, periods and countries with higher duration carry have witnessed slightly higher duration returns in the long-term.
xcatx = ["DU05YCRY_NSA", "DU05YXR_NSA"]
cidx = cids_exp
cr = msp.CategoryRelations(
dfd,
xcats=xcatx,
cids=cidx,
freq="A",
lag=0,
xcat_aggs=["mean", "sum"],
start="2000-01-01",
years=3,
xcat_trims=[5, 50],
)
cr.reg_scatter(
title="Duration carry and returns (5-year tenor) over 3-year periods and countries since 2002",
labels=True,
coef_box="lower right",
xlab="5-year duration carry",
ylab="quarterly 5-year duration returns",
)
DU05YCRY_NSA misses: ['BRL', 'RON'].
DU05YXR_NSA misses: ['BRL', 'RON'].
Appendices #
Appendix 1: Calculations #
Given an arbitrary day \(t\) and tenor \(h\) , the basic formula for the ‘duration’ carry on a fixed receiver position in the main interest rate swap contract traded in the respective currency area is
\( \text{DU\{h\}CRY_NSA}(t) = y_{\text{fix}}(t, h) - y_{\text{float}}(t) - \text{DUR}(t, h) \times \left [y_{\text{fix}}(t,h-1) - y_{\text{fix}}(t,h) \right ] \)
where
-
\(y_{\text{float}}(t,h)\) is the floating interest rate
-
\(y_{\text{fix}}(t,h)\) is the daily fixing of the contract
-
\(\text{DUR}(t, h)\) is the duration of the underlying contract.
Appendix 2: 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).