Macro pressure and rates returns #

This notebook serves as an illustration of the points discussed in the post “The power of macro trends in rates markets” available on the Macrosynergy website. The post highlights the importance of broad macroeconomic trends, such as inflation , economic growth , and credit creation , in influencing shifts in monetary policy. These trends play a crucial role in determining whether monetary policy will lean towards tightening or easing.

The post emphasizes that markets may not always fully anticipate policy shifts that follow macro trends due to a possible lack of attention or conviction. In such cases, macro trends can serve as predictors of returns in fixed-income markets. Even a simple point-in-time macro pressure indicator, which is an average of excess inflation, economic growth, and private credit trends, has exhibited a significant correlation with subsequent interest rate swap returns for 2-years fixed rate receivers in both large and small currency areas.

The post highlights additionally that considering the gap between real rates and macro trend pressure provides an even higher forward correlation and remarkable directional accuracy in predicting fixed income returns.

Imports #

# Uncomment below if running on Kaggle
"""
%%capture
! pip install macrosynergy --upgrade
"""
'\n%%capture\n! pip install macrosynergy --upgrade\n'
import numpy as np
import pandas as pd
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

from timeit import default_timer as timer
from datetime import timedelta, date

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 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 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. For more information see here or use the free dataset on Kaggle

To ensure reproducibility, only samples between January 2000 (inclusive) and May 2023 (exclusive) are considered.

The notebook uses the macrosynergy package, which supports financial market research and the development of trading strategies based on formats and conventions of the J.P. Morgan Macrosynergy Quantamental System (JPMaQS). For full documentation on macrosynergy package check out https://github.com/macrosynergy/macrosynergy or view the notebook on Kaggle for examples.

# Quantamental categories of interest

ecos = [
    "CPIC_SA_P1M1ML12",
    "CPIC_SJA_P3M3ML3AR",
    "CPIC_SJA_P6M6ML6AR",
    "CPIH_SA_P1M1ML12",
    "CPIH_SJA_P3M3ML3AR",
    "CPIH_SJA_P6M6ML6AR",
    "INFTEFF_NSA",
    "INTRGDP_NSA_P1M1ML12_3MMA",
    "INTRGDPv5Y_NSA_P1M1ML12_3MMA",
    "INTRGDPv5Y_NSA_P1M1ML12_6MMA",
    "PCREDITGDP_SJA_D1M1ML12",
    "RGDP_SA_P1Q1QL4_20QMA",
    "RYLDIRS02Y_NSA",
    "RYLDIRS05Y_NSA",
    "PCREDITBN_SJA_P1M1ML12",
]
mkts = [
    "DU02YXR_NSA",
    "DU05YXR_NSA",
    "DU02YXR_VT10",
    "DU05YXR_VT10",
    "EQXR_NSA",
    "EQXR_VT10",
    "FXXR_NSA",
    "FXXR_VT10",
    "FXCRR_NSA",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
]

xcats = ecos + mkts

The description of each JPMaQS category is available either under Macro Quantamental Academy , JPMorgan Markets (password protected), or on Kaggle (limited set of tickers used in this notebook).

# Cross-sections of interest

cids_dm = ["AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NOK", "NZD", "SEK", "USD"]
cids_em = [
    "CLP",
    "COP",
    "CZK",
    "HUF",
    "IDR",
    "ILS",
    "INR",
    "KRW",
    "MXN",
    "PLN",
    "THB",
    "TRY",
    "TWD",
    "ZAR",
]
cids = cids_dm + cids_em
# Download series from J.P. Morgan DataQuery by tickers

start_date = "2000-01-01"
end_date = "2023-05-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")

with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
    df = dq.download(
        tickers=tickers,
        start_date="2000-01-01",
        end_date="2023-01-01",
        suppress_warning=True,
        metrics=["all"],
        report_time_taken=True,
        show_progress=True,
    )
Maximum number of tickers is 624
Downloading data from JPMaQS.
Timestamp UTC:  2024-03-21 14:50:46
Connection successful!
Requesting data: 100%|██████████| 125/125 [00:29<00:00,  4.19it/s]
Downloading data: 100%|██████████| 125/125 [00:29<00:00,  4.21it/s]
Time taken to download data: 	66.39 seconds.
Some expressions are missing from the downloaded data. Check logger output for complete list.
180 out of 2496 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()`.
#  uncomment if running on Kaggle
"""for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))
                                                   
df = pd.read_csv('../input/fixed-income-returns-and-macro-trends/JPMaQS_Quantamental_Indicators.csv', index_col=0, parse_dates=['real_date'])"""
"for dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n                                                   \ndf = pd.read_csv('../input/fixed-income-returns-and-macro-trends/JPMaQS_Quantamental_Indicators.csv', index_col=0, parse_dates=['real_date'])"

Indicators explained #

This example notebook contains a few select categories for a subset of developed and emerging markets: AUD (Australian dollar), CAD (Canadian dollar), CHF (Swiss franc), CLP (Chilean peso), COP (Colombian peso), CZK (Czech Republic koruna), EUR (euro), GBP (British pound), HUF (Hungarian forint), IDR (Indonesian rupiah), ILS (Israeli shekel), INR (Indian rupee), JPY (Japanese yen), KRW (Korean won), MXN (Mexican peso), NOK (Norwegian krone), NZD (New Zealand dollar), PLN (Polish zloty), SEK (Swedish krona), TRY (Turkish lira), TWD (Taiwanese dollar), USD (U.S. dollar) and ZAR (South African rand).

The description of each JPMaQS category is available either under Macro Quantamental Academy , JPMorgan Markets (password protected), or on Kaggle (just for the tickers used in this notebook).

display(df["xcat"].unique())
display(df["cid"].unique())
df["ticker"] = df["cid"] + "_" + df["xcat"]
df.head(3)
array(['CPIC_SA_P1M1ML12', 'CPIC_SJA_P3M3ML3AR', 'CPIC_SJA_P6M6ML6AR',
       'CPIH_SA_P1M1ML12', 'CPIH_SJA_P3M3ML3AR', 'CPIH_SJA_P6M6ML6AR',
       'FXTARGETED_NSA', 'FXUNTRADABLE_NSA', 'FXXR_NSA', 'FXXR_VT10',
       'INFTEFF_NSA', 'INTRGDP_NSA_P1M1ML12_3MMA',
       'INTRGDPv5Y_NSA_P1M1ML12_3MMA', 'PCREDITBN_SJA_P1M1ML12',
       'PCREDITGDP_SJA_D1M1ML12', 'RGDP_SA_P1Q1QL4_20QMA',
       'RYLDIRS02Y_NSA', 'RYLDIRS05Y_NSA', 'DU02YXR_NSA', 'DU02YXR_VT10',
       'DU05YXR_NSA', 'DU05YXR_VT10', 'EQXR_NSA', 'EQXR_VT10',
       'FXCRR_NSA'], dtype=object)
array(['AUD', 'CAD', 'CHF', 'CLP', 'COP', 'CZK', 'EUR', 'GBP', 'HUF',
       'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'MXN', 'NOK', 'NZD', 'PLN',
       'SEK', 'THB', 'TRY', 'TWD', 'USD', 'ZAR'], dtype=object)
real_date cid xcat eop_lag grading mop_lag value ticker
0 2000-01-03 AUD CPIC_SA_P1M1ML12 95.0 2.0 292.0 1.244168 AUD_CPIC_SA_P1M1ML12
1 2000-01-03 AUD CPIC_SJA_P3M3ML3AR 95.0 2.0 186.0 3.006383 AUD_CPIC_SJA_P3M3ML3AR
2 2000-01-03 AUD CPIC_SJA_P6M6ML6AR 95.0 2.0 277.0 1.428580 AUD_CPIC_SJA_P6M6ML6AR

Key hypothesis and the choice of variables #

The basic hypothesis is that excess growth and inflation significantly shape the trend in real and nominal interest rates at all maturities. Positive excesses put upward pressure on rates while negative excesses (shortfalls) exert downward pressure. We call this pressure abstractly “macro trend pressure”. This pressure is unlikely to be fully priced in the market for lack of attention or conviction. In practice, financial markets often neglect the fundamental gravity of rates for the sake of abstract factors, such as carry, and risk management. Below example is a snippet of simple predictions that can be done with selected JPMaQs indicators.

Features (explanatory variables) of the analysis #

Excess growth #

# Excess Growth is a ready-made category available in this dataset. It is defined as the latest estimated "intuitive" GDP growth trend, % over a year ago,
# 3-month moving average minus a long-term median of that country's actual GDP growth rate at that time: based on 5 year lookback of the latter

xcatx = ["INTRGDPv5Y_NSA_P1M1ML12_3MMA"]


msp.view_ranges(
    df,
    cids=cids,
    xcats=xcatx,
    size=(12, 6),
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    df,
    xcats=xcatx,
    cids=cids,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=False,
    title="Latest economic growth trend (intuitive quantamental measure) in excess of 5-year median, % oya, 3-month average",
)
https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/5b2461905d999186fee74a1f7ffd2bc49dbf164f08707ceca97813fc364a1330.png https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/3202f201eff3be03e69aea79a542e2f6485893cf138faeacd1d5591786516992.png

Excess inflation #

In this notebook, excess inflation is defined as the difference between the recorded seasonally and jump-adjusted inflation trend ( CPIC_SJA_P6M6ML6AR ) and the effective inflation target ( INFTEFF_NSA ). The resulting indicator is named CPIC_SJA_P6M6ML6ARvIT .

The excess inflation indicator provides valuable information about inflation dynamics and the extent to which inflation deviates from the desired level. A positive value of CPIC_SJA_P6M6ML6ARvIT indicates that inflation is higher than the target, while a negative value suggests that inflation is lower than the target.

Using the macrosynergy package, we can visualize the newly created indicator.

xcatx = ["INFTEFF_NSA"]
filt1 = df["xcat"].isin(xcatx)
dfb = df[filt1]

infs = [
    "CPIC_SJA_P6M6ML6AR",
]

for inf in infs:
    calcs = [
        f"{inf}vIT = ( {inf} - INFTEFF_NSA )",
    ]
dfa = msp.panel_calculator(df, calcs, cids=cids)
df = msm.update_df(df, dfa)
xcatx = ["CPIC_SJA_P6M6ML6ARvIT"]

msp.view_ranges(
    df,
    cids=cids,
    xcats=xcatx,
    size=(12, 6),
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    df,
    xcats=xcatx,
    cids=cids,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=False,
    title="CPI inflation rates, %ar, versus effective inflation target, market information state",
)
https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/00ceccbd5f96e3ad2293e6e995079e1c3ab527c2053a27f215000fcf1fb449ad.png https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/7493e874530c77b7e68a62932ef21e462a0bd5792d368c24ea0f9345afc1fe40.png

Composite macro trend pressures and rate-pressure gaps #

To create an additional panel of “excess nominal growth,” which serves as a simplified version of the Taylor rule for monetary policy, we calculate the simple average of the excess estimated GDP growth and core CPI trends. This indicator provides a measure of the pressure for monetary tightening (positive values) or easing (negative values).

Using the macrosynergy package, we can visualize the newly created indicator.

calcs = [
    "XGCI = ( INTRGDPv5Y_NSA_P1M1ML12_3MMA + CPIC_SJA_P6M6ML6ARvIT ) / 2",
    "XGCI_NEG = - XGCI",
]

dfa = msp.panel_calculator(df, calcs, cids=cids_dm)
df = msm.update_df(df, dfa)

xcatx = ["XGCI_NEG"]

msp.view_ranges(
    df,
    cids=cids,
    xcats=xcatx,
    size=(12, 6),
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    df,
    xcats=xcatx,
    cids=cids,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=False,
    title="Composite macro trend pressure, % ar, in excess of benchmarks",
)
https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/bd700792fa5fb48ac37bc4244fad474cf2ca37758ea74c126c36b274aa600402.png https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/e62d815f36f3a3086e678b5bb176a263df95f8b210d58c3dcd7a6f40f0c92c7e.png
calcs = [
    "XGCI = ( INTRGDPv5Y_NSA_P1M1ML12_3MMA + CPIC_SJA_P6M6ML6ARvIT ) / 2",
    "XGCI_NEG = - XGCI",
]

dfa = msp.panel_calculator(df, calcs, cids=cids_dm)
df = msm.update_df(df, dfa)

Targets (Response Variables) #

Directional returns #

The below cell visualizes ranges, outliers, and cumulative interest rate swap returns for the 2-years fixed rate receivers.

xcats_sel = ["DU02YXR_NSA"]
msp.view_ranges(
    df,
    cids=cids_em[:8],
    xcats=xcats_sel,
    size=(12, 6),
    kind="box",
    sort_cids_by="std",
    ylab="% daily rate",
    start="2010-01-01",
)
msp.view_timelines(
    df,
    xcats=xcats_sel,
    cids=cids_em[:8],
    ncol=4,
    cumsum=True,
    start="2010-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=False,
    title="Duration return, in % of notional: 2-year maturity ",
    xcat_labels=None,
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/cd2bd10f666796f02420b02bdc61799fa1a549e687326745bab077cb1b0de83e.png https://macrosynergy.com/notebooks.build/trading-factors/macro-pressure-and-rates-returns/_images/b3f8ef5a1cff99b68a709e544b4f934fcfcd4cc308e0ef072c9a68a388e31d5a.png