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.

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_em = cids_latm + cids_emea + cids_emas
cids = sorted(cids_dm + cids_em)
cids_fx = set(cids) - set(cids_dmec + ["EUR", "USD", "CNY"])
main = [
    "POP_NSA_P1Y1YL1",
    "POP_NSA_P1Y1YL1_5YMA",
    "POP_NSA_P1Y1YL1_5YMM",
    "POP_NSA_P1Q1QL4_20QMA",
    "POP_NSA_P1Q1QL4_20QMM",
    "POP_NSA_P1Q1QL4",
    "WFORCE_NSA_P1Y1YL1",
    "WFORCE_NSA_P1Q1QL4",
    "WFORCE_NSA_P1Y1YL1_5YMA",
    "WFORCE_NSA_P1Y1YL1_5YMM",
    "WFORCE_NSA_P1Q1QL4_20QMA",
    "WFORCE_NSA_P1Q1QL4_20QMM",
]
econ = [
    "INTRGDP_NSA_P1M1ML12_3MMA",
    "IMPORTS_SA_P1M1ML12_3MMA",
    "EMPL_NSA_P1M1ML12_3MMA",
    "EMPL_NSA_P1Q1QL4",
]  # economic context
mark = [
    "FXXR_NSA",
    "EQXR_NSA",
    "DU05YXR_NSA",
    "DU05YXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
]  # market links

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

start_date = "1990-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 726
Downloading data from JPMaQS.
Timestamp UTC:  2023-07-14 08:56:24
Connection successful!
Number of expressions requested: 2904
Requesting data: 100%|███████████████████████████████████████████████████████████████| 146/146 [00:46<00:00,  3.13it/s]
Downloading data: 100%|██████████████████████████████████████████████████████████████| 146/146 [01:42<00:00,  1.43it/s]
Download time from DQ: 0:03:03.951158

Availability#

cids_exp = sorted(list(set(cids)))  # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df:  set()
Missing cids for POP_NSA_P1Q1QL4:  {'EUR', 'ILS', 'CNY', 'INR', 'GBP', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'JPY', 'CLP'}
Missing cids for POP_NSA_P1Q1QL4_20QMA:  {'EUR', 'ILS', 'CNY', 'INR', 'GBP', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'JPY', 'CLP'}
Missing cids for POP_NSA_P1Q1QL4_20QMM:  {'EUR', 'ILS', 'CNY', 'INR', 'GBP', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'JPY', 'CLP'}
Missing cids for POP_NSA_P1Y1YL1:  {'NZD', 'AUD', 'HKD', 'CAD'}
Missing cids for POP_NSA_P1Y1YL1_5YMA:  {'NZD', 'AUD', 'HKD', 'CAD'}
Missing cids for POP_NSA_P1Y1YL1_5YMM:  {'NZD', 'AUD', 'HKD', 'CAD'}
Missing cids for WFORCE_NSA_P1Q1QL4:  {'EUR', 'ILS', 'AUD', 'CNY', 'INR', 'GBP', 'HKD', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'CAD', 'JPY', 'CLP'}
Missing cids for WFORCE_NSA_P1Q1QL4_20QMA:  {'EUR', 'ILS', 'AUD', 'CNY', 'INR', 'GBP', 'HKD', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'CAD', 'JPY', 'CLP'}
Missing cids for WFORCE_NSA_P1Q1QL4_20QMM:  {'EUR', 'ILS', 'AUD', 'CNY', 'INR', 'GBP', 'HKD', 'CHF', 'BRL', 'MYR', 'RUB', 'NOK', 'ZAR', 'PLN', 'HUF', 'PHP', 'THB', 'CZK', 'MXN', 'COP', 'USD', 'RON', 'TRY', 'TWD', 'IDR', 'SGD', 'SEK', 'PEN', 'KRW', 'CAD', 'JPY', 'CLP'}
Missing cids for WFORCE_NSA_P1Y1YL1:  {'NZD'}
Missing cids for WFORCE_NSA_P1Y1YL1_5YMA:  {'NZD'}
Missing cids for WFORCE_NSA_P1Y1YL1_5YMM:  {'NZD'}

Most population trend indicators are available by the early 1990s. Notable exceptions are Hungary and South Korea. In comparison, the work force indicators are generally available from the mid-1990s onwards. China is the only cross-section for which work force statistics are available by 1990.

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

print("Last updated:", date.today())
../_images/Demographic trends_16_0.png
Last updated: 2023-07-14
plot = msm.check_availability(
    dfd, xcats=main, cids=cids_exp, start_size=(18, 6), start_years=False
)
../_images/Demographic trends_17_0.png

The vintage grading of employment and work force data is low, as original vintages are not easily accessible and may need to be restored from printed material. These data are not usually watched by markets.

plot = msp.heatmap_grades(
    dfd,
    xcats=main,
    cids=cids_exp,
    size=(18, 6),
    title=f"Average vintage grades from {start_date} onwards",
)
../_images/Demographic trends_19_0.png
xcatx = [indic for indic in main if "POP" in indic]
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), population trends",
    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), population trends",
    start=start_date,
    kind="box",
    size=(16, 4),
)
../_images/Demographic trends_20_0.png ../_images/Demographic trends_20_1.png
xcatx = [indic for indic in main if "WFORCE" in indic]
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), work force indicators",
    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), work force indicators",
    start=start_date,
    kind="box",
    size=(16, 4),
)
../_images/Demographic trends_21_0.png ../_images/Demographic trends_21_1.png

History#

Population growth#

For the purpose of the below presentation, we have renamed the quarterly-frequency annual growth rates to yearly-frequency annual growth rates in order to have a full panel of similar measures across most countries.

olds = ["POP_NSA_P1Q1QL4", "POP_NSA_P1Q1QL4_20QMA"]
news = ["POP_NSA_P1Y1YL1", "POP_NSA_P1Y1YL1_5YMA"]
dfx.replace(to_replace=olds, value=news, inplace=True)

Average population growth has been between 2% and just below -0.5% across the set of relevant markets.

xcatx = ["POP_NSA_P1Y1YL1"]
cidx = cids_exp

msp.view_ranges(
    dfx,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    title="Means and standard deviations of population growth, %oya",
    start="2000-01-01",
)
../_images/Demographic trends_27_0.png
xcatx = ["POP_NSA_P1Y1YL1", "POP_NSA_P1Y1YL1_5YMA"]
cidx = cids_exp

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="Latest report population growth trends, %oya, simple and 5-year averages",
    xcat_labels=["% over a year ago", "% over a year ago, 5-year moving average"],
    title_adj=1.02,
    title_fontsize=27,
    legend_fontsize=17,
    label_adj=0.075,
    title_xadj=0.41,
    ncol=5,
    same_y=False,
    size=(12, 7),
    aspect=1.5,
    all_xticks=True,
)
../_images/Demographic trends_28_0.png

Work force growth#

olds = ["WFORCE_NSA_P1Q1QL4", "WFORCE_NSA_P1Q1QL4_20QMA"]
news = ["WFORCE_NSA_P1Y1YL1", "WFORCE_NSA_P1Y1YL1_5YMA"]
dfx.replace(to_replace=olds, value=news, inplace=True)

While there is an obvious and clear relation between population and work force growth in the medium term, there are also notable differences. In relative terms, the country with the higher population growth does not always record a more rapid expansion of the work force.

xcatx = ["POP_NSA_P1Y1YL1", "WFORCE_NSA_P1Y1YL1"]
cidx = cids_exp

msp.view_ranges(
    dfx,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start="2000-01-01",
    title="Means and standard deviations of population and work force trends",
    xcat_labels=[
        "Reported annual population growth",
        "Reported annual workforce growth",
    ],
)
../_images/Demographic trends_32_0.png

Annual work force growth can be very volatile from year-to-year. Meaningful trends require multi-year averaging.

xcatx = ["WFORCE_NSA_P1Y1YL1", "WFORCE_NSA_P1Y1YL1_5YMA"]
cidx = cids_exp

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    title="Latest reported work force growth trends, %oya, simple and 5-year averages",
    xcat_labels=["%oya", "%oya, 5-year moving average"],
    title_adj=1.02,
    title_xadj=0.44,
    title_fontsize=27,
    legend_fontsize=17,
    label_adj=0.075,
    ncol=5,
    same_y=False,
    size=(12, 7),
    aspect=1.5,
    all_xticks=True,
)
../_images/Demographic trends_34_0.png

Work force growth has mostly been positively correlated across countries.

msp.correl_matrix(
    dfx,
    xcats="WFORCE_NSA_P1Y1YL1",
    cids=cids_exp,
    title="Cross-sectional correlations of workforce trends",
    size=(20, 14),
)
../_images/Demographic trends_36_0.png

Importance#

Relevant research#

“Demographic changes affect nominal yields mainly through real bond yields…The strength of the demographic effect on real yields explains cross-country differences in the comovement between stock and bond markets.” Gozluklu and Morin

“Demographics account for a decrease in the natural real interest rate of about 1.4 percentage points in the euro area compared with the average for the 1980s to 2030 under the baseline calibration. Two channels prevail in providing the downward impact: the increasing scarcity of effective labor input and the growing willingness of individuals to save due to longer life expectancy.” Papetti

Empirical clues#

Population and work force growth trends are mostly used as reference values for calculating trading factors from other quantamental categories, such GDP growth, employment growth or real credit growth.

The below plot shows the relation between “excess employment growth” and subsequent equity index future returns across available markets since 2000. The clear negative relation accords with economic theory, which suggests that labor market tightening leading to less supportive monetary policy and wage cost pressure. The negative relation between employment and future equity returns holds also for employment growth indicators alone, but the subtraction of population or work force trends makes the relation a bit stronger and more precise. Again this is in line with theory and common sense, which suggest that a labor market is only tightening if employment growth faster than the potential work force.

dfd.replace(to_replace="EMPL_NSA_P1Q1QL4", value="EMPL_NSA_P1M1ML12_3MMA", inplace=True)
calcs = ["XEMPL_P1M1ML12_3MMA = EMPL_NSA_P1M1ML12_3MMA - POP_NSA_P1Y1YL1_5YMA"]
dfa = msp.panel_calculator(dfd, calcs=calcs, cids=cids)
dfd = msm.update_df(dfd, dfa)
xcatx = ["XEMPL_P1M1ML12_3MMA", "EQXR_NSA"]
cidx = list(
    set(cids)
    - set(
        [
            "AUD",
            "CAD",
            "CNY",
            "HKD",
            "IDR",
            "INR",
            "NZD",
            "CLP",
            "COP",
            "CZK",
            "HUF",
            "ILS",
            "NOK",
            "PEN",
            "PHP",
            "RON",
            "RUB",
        ]
    )
)  # missing cids from XEMPL or EQXR
cr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
)

cr.reg_scatter(
    title="Employment growth minus population growth and subsequent equity returns across DM and EM since 2000",
    labels=False,
    coef_box="lower left",
    ylab="Equity returns, %qr, next quarter",
    xlab="Latest published employment growth rate (%oya) minus medium-term population growth trend",
)
../_images/Demographic trends_44_0.png
xcatx = ["XEMPL_P1M1ML12_3MMA", "EQXR_NSA"]
cidx = list(
    set(cids_dm) - set(["AUD", "CAD", "NZD", "NOK"])
)  # missing cids from XEMPL or EQXR

cr = msp.CategoryRelations(
    dfd,
    xcats=xcatx,
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
)

cr.reg_scatter(
    title="Excess employment growth and subsequent equity returns in developed markets since 2000",
    labels=False,
    coef_box="lower left",
    ylab="Equity returns, %qr, next quarter",
    xlab="Latest published employment growth rate (%oya) minus medium-term population growth trend",
    prob_est="map",
)
../_images/Demographic trends_45_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).