Labor market tightness #

This category group contains point-in-time measures of latest available information on tightness or slack of labor markets. At present, it contains unemployment rates and unemployment gaps (i.e. unemployment rates in relation to multi-year averages, which form simple model-free reference values for natural unemployment). The state of the labor market is often regarded as a lagging indicator of business cycles, but an important factor of wage and price pressure, and monetary policy.

Unemployment rate #

Ticker : UNEMPLRATE_SA_3MMA

Label : Unemployment rate, sa, 3mma

Definition : Unemployment rate, seasonally adjusted, 3-month moving average

Notes :

  • Unemployment data are taken only from official statistics, not job agencies or surveys.

  • Most countries release monthly-frequency data with fairly short publication lags. However, Norway (NOK) only releases unemployment data quarterly.

  • The UK (GBP) and Peru (PEN) only release unemployment data in 3-month moving averages. However, these are updated at a monthly frequency.

Unemployment gaps #

Ticker : UNEMPLRATE_SA_3MMAv5YMA / _3MMAv10YMA / _3MMAv5YMM / _3MMAv10YMM

Label : Unemployment rate: 3mma vs 5yma / 3mma vs 10yma / 3mma vs 5ymm / 3mma vs 10ymm

Definition : Unemployment rate, seasonally adjusted: 3-month moving average minus the 5-year moving average / 3-month moving average minus the 10-year moving average / 3-month moving average minus the 5-year moving median / 3-month moving average minus the 10-year moving median

Notes :

  • Unemployment data are taken only from official statistics, not job agencies or surveys.

  • Most countries release monthly-frequency data with fairly short publication lags. However, Norway (NOK) only releases unemployment data quarterly.

  • The UK (GBP) and Peru (PEN) only release unemployment data in 3-month moving averages. However, these are updated at a monthly frequency.

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 obtain the data. Here tickers is an array of ticker strings, start_date is the first release date to be considered and metrics denotes the types of information requested.

# 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 = [
    "UNEMPLRATE_SA_3MMA",
    "UNEMPLRATE_SA_3MMAv5YMA",
    "UNEMPLRATE_SA_3MMAv10YMA",
    "UNEMPLRATE_SA_3MMAv5YMM",
    "UNEMPLRATE_SA_3MMAv10YMM",
]
econ = ["INTRGDP_NSA_P1M1ML12_3MMA", "IMPORTS_SA_P1M1ML12_3MMA"]  # economic context
mark = [
    "FXXR_NSA",
    "FXXR_VT10",
    "EQXR_NSA",
    "EQXR_VT10",
    "DU05YXR_NSA",
    "DU05YXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
    "FXXRHvGDRB_NSA",
]  # 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()
    df = downloader.download(
        tickers=tickers,
        start_date=start_date,
        metrics=["value", "eop_lag", "mop_lag", "grading"],
        suppress_warning=True,
    )
    end = timer()

dfd = df

print("Download time from DQ: " + str(timedelta(seconds=end - start)))
Maximum number of tickers is 592
Downloading data from JPMaQS.
Timestamp UTC:  2024-02-23 12:14:32
Connection successful!
Some expressions are missing from the downloaded data. Check logger output for complete list.
468 out of 2368 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 6302 dates are missing.
Download time from DQ: 0:01:22.433435

Availability #

cids_not_exp = ["INR", "IDR", "CNY"]
cids_exp = sorted(
    list(set(cids) - set(cids_dmec + cids_not_exp))
)  # cids expected in category panels
msm.missing_in_df(dfd, xcats=main, cids=cids_exp)
Missing xcats across df:  []
Missing cids for UNEMPLRATE_SA_3MMA:  []
Missing cids for UNEMPLRATE_SA_3MMAv10YMA:  []
Missing cids for UNEMPLRATE_SA_3MMAv10YMM:  []
Missing cids for UNEMPLRATE_SA_3MMAv5YMA:  []
Missing cids for UNEMPLRATE_SA_3MMAv5YMM:  []

Real-time quantamental indicators of labour market tightness are available by the 2000s for most developed countries and many emerging economies. Peru is a notable late starter.

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, 3))

print("Last updated:", date.today())
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/667944be63bed7d1db7611bec0e32c3041f6f610c3c93966c1117dd6055edd67.png
Last updated: 2024-02-23
plot = msm.check_availability(
    dfd, xcats=xcatx, cids=cidx, start_size=(18, 3), start_years=False, start=start_date
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/8728850921577fe621d4737fd9f0493a04e4d8b5dcfef2e4b50b01859d24e9d6.png

Vintage grading is mixed, with Chile the only country with high grade vintages consistently available across indicators.

plot = msp.heatmap_grades(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    size=(18, 3),
    title=f"Average vintage grades, from {start_date} onwards",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/a5eb5700169308dacde162ca0afeaed21c92f0878a7d5747d958743fcb9bdd70.png
msp.view_ranges(
    dfd,
    xcats=["UNEMPLRATE_SA_3MMA"],
    cids=cids_exp,
    val="eop_lag",
    title="End of observation period lags (ranges of time elapsed since end of observation period in days)",
    start="2000-01-01",
    kind="box",
    size=(16, 4),
)
msp.view_ranges(
    dfd,
    xcats=["UNEMPLRATE_SA_3MMA"],
    cids=cids_exp,
    val="mop_lag",
    title="Median of observation period lags (ranges of time elapsed since middle of observation period in days)",
    start="2000-01-01",
    kind="box",
    size=(16, 4),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/54469259f505446101c2ebf35acca50ebb6caf259793280fdd170733d959a456.png https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/7a550b4058936994fa4502af92c30eaf96828b908b7e5f9f6050721e5402e5db.png
xcatx = [
    "UNEMPLRATE_SA_3MMAv5YMA",
    "UNEMPLRATE_SA_3MMAv10YMM",
    "UNEMPLRATE_SA_3MMAv5YMA",
    "UNEMPLRATE_SA_3MMAv5YMM",
]

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="2000-01-01",
    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="2000-01-01",
    kind="box",
    size=(16, 4),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/ed8fae43580fffe32267f1a97c1e6851715283ef706472af602fd31d114d2fcc.png https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/a398d231770ec0c8a92a5063ce5ced988ab5d88802dc4401360962dec1ffa098.png

History #

Unemployment rate #

Long-term averages are vastly different across countries, reflecting differences in labor market structure, data collection, and social security. Countries with regulated labor markets and comprehensive unemployment benefits tend to have higher long-term unemployment rates.

xcatx = ["UNEMPLRATE_SA_3MMA"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start=start_date,
    title="Means and standard deviations of unemployment rates since 2000",
    xcat_labels=["Seasonally adjusted unemployment rate, 3-month moving average"],
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/91dc5e05ecf2ab8ba974f154aa02d4644ad6402fb6e653fa9e0cc0271804aee1.png

Unemployment trends have displayed long cycles and their 3-month moving averages show much less short-term volatility than comparable production indices.

xcatx = ["UNEMPLRATE_SA_3MMA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Unemployment rates, seasonally adjusted, 3-month moving average since 2000",
    title_fontsize=27,
    ncol=4,
    same_y=False,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/5687a249f65a5e03300091a0764217a66ac10545c6b38004f08adf449f41490d.png

Unemployment rate gaps versus 5-10 year averages #

Focusing on differences between unemployment rates and multi-year averages naturally removes structural differences in recorded means. These gaps do not only capture the state of unemployment, but are also affected by any long-term secular trend. Economies with economic system transformation, such as in central and eastern Europe, have posted significant secular declines in joblessness in past decades.

xcatx = ["UNEMPLRATE_SA_3MMAv5YMA", "UNEMPLRATE_SA_3MMAv10YMA"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start=start_date,
    title="Means and standard deviations of unemployment rate gaps, since 2000",
    xcat_labels=[
        "3-month moving average vs 5-year moving average, (sa)",
        "3-month moving average vs 10-year moving average, (sa)",
    ],
    kind="bar",
    size=(16, 8),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/2035a74298f3eda4558ff5e6920625de874b954e2f9fb2a1e7a3f5c8405ef3eb.png

Unemployment gaps follow broad long-term business cycles.

xcatx = ["UNEMPLRATE_SA_3MMAv5YMA", "UNEMPLRATE_SA_3MMAv10YMA"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Unemployment rates, 3-month moving average, seasonally adjusted, gap versus multi-year averages",
    xcat_labels=["5-year lookback", "10-year lookback"],
    legend_fontsize=18,
    title_fontsize=27,
    ncol=4,
    same_y=True,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/ffee764136fd812fc39fa51bdeb163c525b34a37c383677c9214e800959edbab.png

Interestingly, unemployment gaps, unlike economic growth trends, are not strongly positively correlated across countries. This plausibly reflects their greater dependence on domestic services and the different structure of labor markets, some of which require long notice periods for employment reductions.

cidx = cids_exp
msp.correl_matrix(
    dfd,
    xcats="UNEMPLRATE_SA_3MMAv5YMA",
    cids=cidx,
    size=(20, 14),
    start=start_date,
    title="Cross-sectional correlation of unemployment rate gaps, since 2000",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/f9546555946f23e9ff8eaf84970a12bf5a75de0177c8e97e6cce196bf99919f0.png

Unemployment rate gaps versus 5-10 year medians #

Empirically, the use of medians rather than means for long-term moving averages does not greatly change the unemployment gap time series. However, it reduces the impact of outlier periods on the multi-year average.

xcatx = ["UNEMPLRATE_SA_3MMAv5YMM", "UNEMPLRATE_SA_3MMAv10YMM"]
cidx = cids_exp

msp.view_ranges(
    dfd,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start=start_date,
    title="Means and standard deviations of unemployment rate gaps, since 2000",
    xcat_labels=[
        "3-month moving average vs 5-year moving median, (sa)",
        "3-month moving average vs 10-year moving median, (sa)",
    ],
    kind="bar",
    size=(16, 8),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/4c83b45e38e6a8025a9bc3596a47d1f0621842af47d492fed94e0d507c16c6b2.png
xcatx = ["UNEMPLRATE_SA_3MMAv5YMM", "UNEMPLRATE_SA_3MMAv10YMM"]
cidx = cids_exp

msp.view_timelines(
    dfd,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Unemployment rates, 3-month moving averages, seasonally-adjusted, gap versus multi-year medians",
    xcat_labels=["5-year lookback", "10-year lookback"],
    legend_fontsize=18,
    title_fontsize=27,
    ncol=4,
    same_y=True,
    size=(12, 7),
    aspect=1.7,
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/08c2a79107f00cb6a2633a5836b5988a9e2180af41b9cf3a8a29cae9a6c65746.png

Importance #

Empirical clues #

Relatively high unemployment should benefit broad equity return performance, as it supports accommodative monetary policy conditions and reduces wage pressure. Indeed, there has been significant positive correlation between unemployment rates and subsequent equity returns across developed markets.

cids_eq = dfd[dfd["xcat"] == "EQXR_NSA"]["cid"].unique()
cidx = list(set(cids_dmca).intersection(set(cids_eq)))

cr = msp.CategoryRelations(
    dfd,
    xcats=["UNEMPLRATE_SA_3MMAv10YMM", "EQXR_NSA"],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2002-01-01",
)
cr.reg_scatter(
    title="Unemployment gaps and subsequent equity index returns in developed countries since 2002",
    labels=False,
    coef_box="lower left",
    ylab="Local equity index future return, following quarter, %qr",
    xlab="Unemployment rates versus 10-year moving median, end of quarter",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/d92892b3fdb3d66716dc678e51c9d2610e004c01c960d395422f6a8ef33d8e8b.png

Evidence of a positive correlation also prevails amongst emerging markets, but it is weaker.

cids_eq = dfd[dfd["xcat"] == "EQXR_NSA"]["cid"].unique()
cids_sel = list(set(cids_em) & set(cids_eq) - set(["CNY", "INR"]))
cr = msp.CategoryRelations(
    dfd,
    xcats=["UNEMPLRATE_SA_3MMAv10YMM", "EQXR_NSA"],
    cids=cids_sel,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2002-01-01",
)

cr.reg_scatter(
    title="Unemployment gaps and subsequent equity index returns in emerging countries since 2002",
    labels=False,
    coef_box="lower left",
    ylab="Local equity index future return, following quarter, %qr",
    xlab="Unemployment rates versus 10-year moving median, end of quarter",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/b17c7555530c12f679cdf20b7d58c44221a1d92aaf82fa7f5cca7265a810c305.png

Greater unemployment gaps lead to periods of reduced public spending, leading to a slowdown in economic activity and greater economic instability. This creates risk from the perspective of an investor and can consequently reduce demand for the currency, resulting in smaller FX forward rates against the dollar. This is more easily seen in JPMaQS by regressing FX forward returns (or vol-targeted returns) against unemployment gap differentials vs the natural FX benchmark for the currency area.

See the notes for ‘FX forward return in % of notional’ here for more information on the benchmarks used for each cross-section.

# create blacklist for targeted/untradeable currencies

dfb = dfd[dfd["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
    :, ["cid", "xcat", "real_date", "value"]
]
dfba = (
    dfb.groupby(["cid", "real_date"])
    .aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
    .reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
# Feature engineer the unemployment gap differentials vs natural benchmarks

cidx = cids_exp

cids_eur = ["CHF", "CZK", "HUF", "NOK", "PLN", "RON", "SEK"]  # EUR benchmark cids
cids_eud = ["GBP", "TRY", "RUB"]  # mean(EUR,USD) benchmark cids
cids_usd = list(set(cidx) - set(cids_eur) - set(cids_eud))  # USD benchmark cids

xcatx = ["UNEMPLRATE_SA_3MMAv5YMM", "UNEMPLRATE_SA_3MMAv10YMM"]
xcatx += ["UNEMPLRATE_SA_3MMAv5YMA", "UNEMPLRATE_SA_3MMAv10YMA"]

for xc in xcatx:
    calc_eur = [f"{xc}vBM = {xc} - iEUR_{xc}"]
    calc_usd = [f"{xc}vBM = {xc} - iUSD_{xc}"]
    calc_eud = [f"{xc}vBM = {xc} - 0.5 * ( iEUR_{xc} + iUSD_{xc} )"]

    dfa_eur = msp.panel_calculator(dfd, calcs=calc_eur, cids=cids_eur)
    dfa_usd = msp.panel_calculator(dfd, calcs=calc_usd, cids=cids_usd)
    dfa_eud = msp.panel_calculator(dfd, calcs=calc_eud, cids=cids_eud)

    dfa = pd.concat([dfa_eur, dfa_usd, dfa_eud])
    dfd = msm.update_df(dfd, dfa)
# Unemployment gap, no benchmark

xcatx = ["UNEMPLRATE_SA_3MMAv5YMA", "FXXR_VT10"]
cidx = list(set(cids_exp) - set(["USD"]))

cr = msp.CategoryRelations(
    df=dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,
    freq="Q",
    lag=1,
    xcat_aggs=["mean", "sum"],
)
cr.reg_scatter(
    coef_box="lower left",
    prob_est="map",
    title="Unemployment gaps and subsequent FX forward returns, 10% vol-target, since 2000",
    xlab="Unemployment rates vs 5-year moving average, quarterly average",
    ylab="Next-quarter cumulative vol-targeted FX forward returns",
)

# Unemployment gap vs natural benchmark

xcatx = ["UNEMPLRATE_SA_3MMAv5YMAvBM", "FXXR_VT10"]
cidx = list(set(cids_exp) - set(["USD"]))

cr = msp.CategoryRelations(
    df=dfd,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,
    freq="Q",
    lag=1,
    xcat_aggs=["mean", "sum"],
)
cr.reg_scatter(
    coef_box="lower left",
    prob_est="map",
    title="Unemployment gap differentials vs natural benchmarks and subsequent FX forward returns, 10% vol-target, since 2000",
    xlab="Unemployment gaps, against 5-year moving average, vs natural FX benchmark, quarterly average",
    ylab="Next-quarter cumulative vol-targeted FX forward returns",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/076e13fd7505ffa602e3120cfc4700a0b19438bea0bca0074255af7c744e5a2b.png https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/19433d9308728a589de009021e847eb772362fa5c73f060a87ab6be58f0ad226.png

For a number of cross-sections, there is evidence in favour of using the inverse sign of the unemployment gap differentials as a simple feature for predicting the direction of subsequent cumulative FX forward returns (with vol-targeting).

xcatx = ["UNEMPLRATE_SA_3MMAv5YMAvBM", "FXXR_VT10"]
cidx = list(set(cids_exp) - set(["USD"]))

sr = mss.SignalReturnRelations(
    df=dfd,
    rets=xcatx[1],
    sigs=[xcatx[0], "UNEMPLRATE_SA_3MMAv5YMA"],
    cids=cidx,
    sig_neg=[True, True],
    start="2000-01-01",
    blacklist=fxblack,
    agg_sigs="mean",
    freqs="M",
)
sr.accuracy_bars()
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/0c471e3b95d3775c34913ca6d0ee6f8a29e1522a7be13318b984a500d1932ee7.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).