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)
# Quantamental categories of interest

main = [
    "CPIXFE_SA_P1M1ML12",
    "CPIXFE_SJA_P3M3ML3AR",
    "CPIXFE_SJA_P6M6ML6AR",
    "CPIXFE_SA_P1M1ML12_D1M1ML3",
]
econ = [
    "CPIC_SA_P1M1ML12",
    "CPIC_SJA_P3M3ML3AR",
    "CPIC_SJA_P6M6ML6AR",
    "CPIC_SA_P1M1ML12_D1M1ML3",
]  # economic context
mark = ["RIR_NSA", "DU05YXR_NSA", "DU05YXR_VT10"]  # market links

xcats = main + econ + mark
# Download series from J.P. Morgan DataQuery by tickers

start_date = "1995-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()
    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 407
Downloading data from JPMaQS.
Timestamp UTC:  2024-02-23 11:32:01
Connection successful!
Requesting data: 100%|██████████| 82/82 [00:19<00:00,  4.31it/s]
Downloading data: 100%|██████████| 82/82 [00:22<00:00,  3.57it/s]
Some expressions are missing from the downloaded data. Check logger output for complete list.
172 out of 1628 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 7607 dates are missing.
Download time from DQ: 0:00:47.589194

Availability#

cids_exp = sorted(list(set(cids) - set(cids_dmec)))  # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df:  []
Missing cids for CPIXFE_SA_P1M1ML12:  []
Missing cids for CPIXFE_SA_P1M1ML12_D1M1ML3:  []
Missing cids for CPIXFE_SJA_P3M3ML3AR:  []
Missing cids for CPIXFE_SJA_P6M6ML6AR:  []

Real-time quantamental indicators of consumer price trends for developed markets are typically available by the late 1990s. Swiss core inflation only starts in 2003, however. The EM series mostly begin in the late 1990s or early 2000s. India is a notable late starter, as CPI was not widely followed before the 2010s.

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=(20, 3))

print("Last updated:", date.today())
../_images/Consistent core CPI trends_19_0.png
Last updated: 2024-02-23
plot = msm.check_availability(
    dfd, xcats=main, cids=cids_exp, start_size=(20, 2), start_years=False
)
../_images/Consistent core CPI trends_20_0.png

Average grades are currently quite mixed across countries and times.

plot = msp.heatmap_grades(
    dfd,
    xcats=main,
    cids=cids_exp,
    size=(19, 2),
    title=f"Average vintage grades from {start_date} onwards",
)
../_images/Consistent core CPI trends_22_0.png
for x in main:
    xcatx = [x]
    msp.view_ranges(
        dfd,
        xcats=xcatx,
        cids=cids_exp,
        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=cids_exp,
        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/Consistent core CPI trends_23_0.png ../_images/Consistent core CPI trends_23_1.png ../_images/Consistent core CPI trends_23_2.png ../_images/Consistent core CPI trends_23_3.png ../_images/Consistent core CPI trends_23_4.png ../_images/Consistent core CPI trends_23_5.png ../_images/Consistent core CPI trends_23_6.png ../_images/Consistent core CPI trends_23_7.png

History#

Annual consistent core CPI inflation#

Quantamental indicators of annual consistent core inflation have posted pronounced cycles and trends. For most countries, they have been similar to JPMaQS indicators of conventional core inflation, but there have also been notable differences.

While consistent core CPI excludes only specific food and energy prices, conventional core CPI often excludes prices or uses methods that are of particular local relevance. This explains the wide divergences in some countries.

xcatx = ["CPIXFE_SA_P1M1ML12", "CPIC_SA_P1M1ML12"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start=start_date,
    kind="bar",
    size=(16, 8),
    title="Means and standard deviations of annual consistent and local conventional core CPI inflations since 1995",
    xcat_labels=[
        "Annual consistent core CPI inflation",
        "Local conventional core CPI inflation",
    ],
)
../_images/Consistent core CPI trends_27_0.png
xcatx = ["CPIXFE_SA_P1M1ML12", "CPIC_SA_P1M1ML12"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Annual consistent and local conventional core CPI inflations, % change of latest release over a year ago",
    xcat_labels=[
        "Annual consistent core CPI inflation",
        "Local conventional core CPI inflation",
    ],
    legend_fontsize=17,
    title_fontsize=27,
    label_adj=0.075,
    ncol=4,
    same_y=False,
    size=(16, 8),
    aspect=1.7,
    all_xticks=True,
)
../_images/Consistent core CPI trends_28_0.png

Unlike for conventional CPI measures, cross-country correlations of consistent core CPI have not always been positive, emphasizing that this metric focuses more on local inflation trends and less on global effects, such as commodity price fluctuations and harvest conditions. This makes consistent core inflation a suitable basis for detecting inflation trend differentials.

xcatx = ["CPIXFE_SA_P1M1ML12"]
cidx = cids_exp

msp.correl_matrix(
    dfd,
    xcats=xcatx,
    cids=cidx,
    size=(20, 14),
    title="Cross-sectional correlations of annual consistent core CPI inflation since 1995",
)
../_images/Consistent core CPI trends_30_0.png

Change in consistent core consumer price inflation#

The dynamics of consistent core CPI changes and core inflation changes (according to local convention) can be very different for different countries.

xcatx = ["CPIC_SA_P1M1ML12_D1M1ML3", "CPIXFE_SA_P1M1ML12_D1M1ML3"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="3-month difference between conventional and consistent consumer price inflation, % over a year ago",
    title_fontsize=27,
    legend_fontsize=17,
    label_adj=0.075,
    xcat_labels=["Conventional", "Consistent"],
    ncol=4,
    same_y=False,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
../_images/Consistent core CPI trends_38_0.png

Cross-country correlation of inflation changes is much more diverse compared to the conventional inflation changes.

xcatx = "CPIXFE_SA_P1M1ML12_D1M1ML3"
cidx = cids_exp

msp.correl_matrix(
    dfd,
    xcats=xcatx,
    cids=cidx,
    title="Cross-sectional correlation of 3-month changes in consistent core consumer price inflation, since 2000",
    start="2000-01-01",
    size=(20, 14),
)
../_images/Consistent core CPI trends_40_0.png

Importance#

Empirical clues#

Core inflation trend differences account for a large portion of real short-term interest rate variations across time and countries in the developed and emerging worlds.

xcatx = ["CPIXFE_SJA_P6M6ML6AR", "RIR_NSA"]
cidx = cids_exp

cr_crr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="Q",
    lag=0,
    xcat_aggs=["mean", "mean"],
    start=start_date,
    xcat_trims=[20, 25],
)

cr_crr.reg_scatter(
    title="Core CPI trends and concurrent local short-term rates, global panel since 1995",
    labels=False,
    coef_box="upper left",
    xlab="Core CPI, %6m/6m, saar, quarterly average",
    ylab="Main local 1-month interest rate, quarterly average",
    reg_robust=False,
)
../_images/Consistent core CPI trends_46_0.png

Short-term consistent core inflation trends have been negatively correlated with subsequent IRS receiver returns. This is in line with the intuition that markets are not fully information efficient and that fixed income returns are reduced by lagged effects of high reported meaningful inflation trends.

cr = msp.CategoryRelations(
    dfd,
    xcats=["CPIXFE_SJA_P3M3ML3AR", "DU05YXR_VT10"],
    cids=list(set(cids_exp) - set(["BRL", "PEN", "PHP", "RON"])),
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    fwin=1,
    start="2000-01-01",
    years=None,
    xcat_trims=[20, 30],  # contain outliers
)

cr.reg_scatter(
    title="Core CPI trends and subsequent quarterly IRS returns since 2000",
    labels=False,
    coef_box="lower left",
    xlab="CPI trend, %3m/3m, saar",
    ylab="Cumulative 5-year IRS receiver return, next month",
    prob_est="map",
)
../_images/Consistent core CPI trends_48_0.png

Importantly, there is also a slight negative relation between core CPI trends and subsequent IRS returns in relative terms. That means that a high core CPI trend in one currency versus a global basket is followed by an underperformance of fixed income positions in that area.

dfa = msp.make_relative_value(
    dfd, xcats=["CPIXFE_SJA_P3M3ML3AR", "DU05YXR_VT10"], cids=cids_exp
)
dfxx = msm.update_df(dfd, dfa)
The category, DU05YXR_VT10, is missing ['PEN', 'RON', 'PHP', 'BRL'] from the requested basket. The new basket will be ['AUD', 'CAD', 'CHF', 'CLP', 'CNY', 'COP', 'CZK', 'EUR', 'GBP', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PLN', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'TWD', 'USD', 'ZAR'].
cr = msp.CategoryRelations(
    dfxx,
    xcats=["CPIXFE_SJA_P3M3ML3ARR", "DU05YXR_VT10R"],
    cids=list(set(cidx) - set(["BRL", "PEN", "PHP", "RON"])),
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    fwin=1,
    start=start_date,
    years=None,
    xcat_trims=[20, 25],  # contain outliers
)

cr.reg_scatter(
    title=f"Relative core CPI trends and subsequent relative monthly IRS returns since 2000",
    labels=False,
    coef_box="lower left",
    xlab="Relative 3-month seasonally and jump-adjusted consistent core CPI trends, % change at month end",
    ylab="Cumulative monthly 5-year IRS returns (next three months)",
    prob_est="map",
)
../_images/Consistent core CPI trends_51_0.png

Appendices#

Appendix 1: Indicator calculations#

First obtain the seasonally adjusted headline CPI series from the raw data. For some cross-sections the adjustment is performed in JPMaQS, whilst occasionally this is prevalent in the underlying raw data. Secondly, each non-core CPI component (and respective weights) are obtained from the raw data. Whilst these components may vary for different cross-sections, these are broadly:

  • food and non-alcoholic beverages

  • alcoholic beverages and tobacco

  • energy

  • fuel for personal transport

Next, the following two series’ are calculated:

  • Headline CPI, seasonally-adjusted, 1-year % change

  • Non-core CPI, seasonally-adjusted, 1-year % change

The non-core CPI series is proportional to a weighted sum of the 1-year % change in seasonally-adjusted non-core CPI component contributions.

Finally, the total sum of weights is calculated and the annual consistent core CPI is given by:

(1)#\[\begin{equation} \text{Annual consistent core CPI} = 100 \times \text{cprod} \left (1+ \frac{\text{1 year \% change in headline CPI} - \text{1 year \% change in non-core CPI}}{1-\frac{1}{100}\times\text{Total weights}} \right) \end{equation}\]

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).