Labor market dynamics #

This category group contains real-time measures of changes in employment and unemployment. Whilst labor market dynamics are often seen as lagging indicators of business cycles, they are also indicative of the impact of the economic situation on private households, popular sentiment and labor cost.

Employment growth #

Ticker : EMPL_NSA_P1M1ML12 / _P1M1ML12_3MMA / _P1Q1QL4

Label : Employment growth: %oya / %oya, 3mma / %oya, quarterly

Definition : Main measure of employment growth: % over a year ago / % over a year ago, 3-month moving average / % over a year ago, quarterly

Notes :

  • Employment data is taken only from official statistics, not job agencies or surveys.

  • Most countries release monthly-frequency data with fairly short publication lags. However, the following currency areas only release quarterly data: Switzerland (CHF), Czech Republic (CZK), the Eurozone (EUR), Mexico (MXN), Norway (NOK), New Zealand (NZD), the Phillipines (PHP), Singapore (SGD), Thailand (THB) and South Africa (ZAR). There are no employment growth rates on a monthly frequency for these countries.

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

  • Employment growth has not been calculated for countries and periods with semi-annual and annual data, most prominently China and Indonesia.

Unemployment growth #

Ticker : UNEMPLRATE_NSA_3MMA_D1M1ML12 / _D1Q1QL4

Label : Unemployment rate change: oya, 3mma / oya, quarterly

Definition : Change in unemployment rate: over a year ago, 3-month moving average / over a year ago, quarterly

Notes :

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

  • Most countries release monthly-frequency data with fairly short publication lags. However, the following currency areas only release quarterly data: the Phillipines (PHP), Singapore (SGD) and South Africa (ZAR). There are no unemployment growth rates on a monthly frequency for these countries.

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

  • Unemployment growth has not been calculated for countries that only provide semi-annual and annual data, most prominently China and Indonesia.

Ticker : UNEMPLRATE_SA_D3M3ML3 / _D6M6ML6 / _D1Q1QL1 / _D2Q2QL2

Label : Unemployment growth, seasonally adjusted: difference 3 months over 3 months ago / difference 6 months over 6 months ago / difference 1 quarter over 1 quarter ago / difference 2 quarters over 2 quarters ago

Definition : Change in seasonally adjusted unemployment rate: 3 months over previous 3 months / 6 months over previous 6 months / quarter over previous quarter / 2 quarters over previous two quarters

Notes :

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

  • Most countries release monthly-frequency data with fairly short publication lags. However, the following currency areas only release quarterly data: the Phillipines (PHP), Thailand (THB) and South Africa (ZAR). There are no unemployment growth rates on a monthly frequency for these countries.

  • Brazil (BRL), the UK (GBP) and Peru (PEN) only release seasonally adjusted unemployment in 3-month moving averages. However, these are updated at a monthly frequency.

  • Unemployment growth has not been calculated for countries and periods with semi-annual and annual data, most prominently China and Indonesia.

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 macrosynergy.management as msm
import macrosynergy.panel as msp
import macrosynergy.signal as mss
import macrosynergy.pnl as msn
import macrosynergy.visuals as msv

from macrosynergy.download import JPMaQSDownload

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

# For Equity analysis:

cids_g3 = ["EUR", "JPY", "USD"]  # DM large currency areas
cids_dmes = ["AUD", "CAD", "CHF", "GBP", "SEK"]  # Smaller DM equity countries
cids_dmeq = cids_g3 + cids_dmes  # DM equity countries

# For FX analysis:
cids_nofx = ["USD", "EUR", "CNY", "SGD", "HKD"]
cids_dmfx = list(set(cids_dmca) - set(cids_nofx)) # DM currencies excluding USD, EUR
cids_emfx = list(set(cids_em) - set(cids_nofx))

cids = sorted(cids_dm + cids_em)
# Quantamental categories of interest

main = [
    "EMPL_NSA_P1M1ML12",
    "EMPL_NSA_P1M1ML12_3MMA",
    "EMPL_NSA_P1Q1QL4",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12",
    "UNEMPLRATE_NSA_D1Q1QL4",
    "UNEMPLRATE_SA_D3M3ML3",
    "UNEMPLRATE_SA_D6M6ML6",
    "UNEMPLRATE_SA_D1Q1QL1",
    "UNEMPLRATE_SA_D2Q2QL2",
]
econ = ["INTRGDP_NSA_P1M1ML12_3MMA", 
        "IMPORTS_SA_P1M1ML12_3MMA", 
        "UNEMPLRATE_SA_3MMA", 
        "UNEMPLRATE_SA_3MMAv5YMA",
        "WFORCE_NSA_P1Y1YL1_5YMM"]  # economic context
mark = [
    "FXXR_NSA",
    "EQXR_NSA",
    "DU05YXR_NSA",
    "DU05YXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
]  # market links

xcats = main + econ + mark

cids_co = ["GLD"] # gold
xcats_co = ["COXR_VT10", "COXR_NSA"] # Commodity future returns

tix_co = [c + "_" + x for c in cids_co for x in xcats_co]  # gold futures returns
# Download series from J.P. Morgan DataQuery by tickers

tickers = [cid + "_" + xcat for cid in cids for xcat in xcats]
tickers = tickers + tix_co

print(f"Maximum number of tickers is {len(tickers)}")
start_date="2000-01-01"


# 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=start_date,
        suppress_warning=True,
        metrics=["all"],
        show_progress=True,
    )
Maximum number of tickers is 762
Downloading data from JPMaQS.
Timestamp UTC:  2024-06-04 15:47:26
Connection successful!
Requesting data: 100%|██████████| 153/153 [00:34<00:00,  4.37it/s]
Downloading data: 100%|██████████| 153/153 [00:14<00:00, 10.63it/s] 
Some expressions are missing from the downloaded data. Check logger output for complete list.
1128 out of 3048 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. 
6374 out of 6374 dates are missing.

Availability #

cids_exp = sorted(
    list(set(cids) - set(cids_dmec + ["CNY", "IDR", "INR"]))
)  # cids expected in category panels
msm.missing_in_df(df, xcats=main, cids=cids_exp)
No missing XCATs across DataFrame.
Missing cids for EMPL_NSA_P1M1ML12:             ['CHF', 'CZK', 'EUR', 'GBP', 'MXN', 'NOK', 'NZD', 'PEN', 'PHP', 'SGD', 'THB', 'ZAR']
Missing cids for EMPL_NSA_P1M1ML12_3MMA:        ['CHF', 'CZK', 'EUR', 'MXN', 'NOK', 'NZD', 'PHP', 'SGD', 'THB', 'ZAR']
Missing cids for EMPL_NSA_P1Q1QL4:              ['AUD', 'BRL', 'CAD', 'CLP', 'COP', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY', 'KRW', 'MYR', 'PEN', 'PLN', 'RON', 'RUB', 'SEK', 'TRY', 'TWD', 'USD']
Missing cids for UNEMPLRATE_NSA_3MMA_D1M1ML12:  ['NZD', 'PHP', 'SGD', 'ZAR']
Missing cids for UNEMPLRATE_NSA_D1Q1QL4:        ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'COP', 'CZK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'PEN', 'PLN', 'RON', 'RUB', 'SEK', 'THB', 'TRY', 'TWD', 'USD']
Missing cids for UNEMPLRATE_SA_D1Q1QL1:         ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'COP', 'CZK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PEN', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'TRY', 'TWD', 'USD']
Missing cids for UNEMPLRATE_SA_D2Q2QL2:         ['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'COP', 'CZK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PEN', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'TRY', 'TWD', 'USD']
Missing cids for UNEMPLRATE_SA_D3M3ML3:         ['PHP', 'THB', 'ZAR']
Missing cids for UNEMPLRATE_SA_D6M6ML6:         ['PHP', 'THB', 'ZAR']

Availability of real-time quantamental indicators of labor market dynamics, either quarterly or monthly, is very diverse, with some series’ going back to the 1970s and others only starting in the 2010s.

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(df, xcats=xcatx, cids=cidx)
dfs = msm.check_startyears(dfx)
msm.visual_paneldates(dfs, size=(18, 6))
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/518fcbff0f9baa161c652166bc1b9ddd9c263442870eba8cadc75599083f148f.png
plot = msm.check_availability(
    df, xcats=xcatx, cids=cidx, start_size=(18, 6), start_years=False, start=start_date
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/065db31f7d94cb83218161f4a9b34c0d115e97c318922210df620f9076950a11.png

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

plot = msp.heatmap_grades(
    df,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    size=(16, 4),
    title=f"Average vintage grades, from {start_date} onwards",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/39349a932ebdec2eb9f6320c74d1cc4dde7a2a894ee039d71de7713a78f455f4.png
xcatx = ["EMPL_NSA_P1M1ML12", "EMPL_NSA_P1M1ML12_3MMA", "EMPL_NSA_P1Q1QL4"]

msp.view_ranges(
    df,
    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="2000-01-01",
    kind="box",
    size=(16, 4),
)
msp.view_ranges(
    df,
    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="2000-01-01",
    kind="box",
    size=(16, 4),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/35df4238ba622070bdd9201ed13bf9f5365a6567b094794c1563f4e5dea5dde2.png https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/d92f9ffcc2c973ebeb07ccb883b838b3e1db1a0246dd143a8d8036949b273612.png
xcatx = [
    "UNEMPLRATE_SA_D3M3ML3",
    "UNEMPLRATE_SA_D6M6ML6",
    "UNEMPLRATE_SA_D1Q1QL1",
    "UNEMPLRATE_SA_D2Q2QL2",
]

msp.view_ranges(
    df,
    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="2000-01-01",
    kind="box",
    size=(16, 4),
)
msp.view_ranges(
    df,
    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="2000-01-01",
    kind="box",
    size=(16, 4),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/21614a0141e936023be3756da8da1445b602aecde078b0958192c58e8a0612b6.png https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/6192c6917892a69636f597a8b34da5eaee67580175c9138e11fcb0024f8cc815.png

For the purpose of the below presentation, we have renamed quarterly-frequency indicators to approximate monthly equivalents in order to have a full panel of similar measures across most countries. The two series’ are not identical but are close substitutes.

olds = [

    "EMPL_NSA_P1Q1QL4", "UNEMPLRATE_NSA_D1Q1QL4"
]
news = [

    "EMPL_NSA_P1M1ML12_3MMA","UNEMPLRATE_NSA_3MMA_D1M1ML12"
]



dfx = df.replace(to_replace=olds, value=news)

scols = ["cid", "xcat", "real_date", "value"]
dfx = dfx[scols].copy().sort_values(["cid", "xcat", "real_date"])
dfx["ticker"] = dfx["cid"] + "_" + dfx["xcat"]

History #

Employment growth #

Employment growth has been positive across all currency areas since 2000 or over the available sample periods. Fluctuations have been larger in American economies than elsewhere.

cidx = list(set(cids_exp) - set(["TWD", "CZK"]))
xcatx = ["EMPL_NSA_P1M1ML12_3MMA"]

msp.view_ranges(
    dfx,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    start=start_date,
    kind="bar",
    size=(16, 8),
    title="Means and standard deviations of employment growth since 1990",
    xcat_labels=["Employment growth, % over a year ago, 3-month moving average"],
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/c40747c7948d455896c27b4567aeeac00657f1f9e3b207c3ef8e40ad98dc45db.png
cidx = list(set(cids_exp) - set(["TWD", "CZK"]))
xcatx = ["EMPL_NSA_P1M1ML12", "EMPL_NSA_P1M1ML12_3MMA"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Employment growth, % over a year ago",
    xcat_labels=["Monthly (if available)", "3-month moving average"],
    ncol=4,
    same_y=False,
    legend_fontsize=17,
    title_fontsize=27,
    size=(12, 7),
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/a9a8b8b1a98bf83e056e1c54f03b80a7924fb5ce3f00650a61735abce2a432d3.png

Information states of employment growth has been predominantly positively correlated across currency markets. All countries, except for Thailand, have displayed positive correlation with U.S. payroll growth.

msp.correl_matrix(
    dfx,
    xcats="EMPL_NSA_P1M1ML12_3MMA",
    cids=cidx,
    size=(20, 14),
    start=start_date,
    title="Cross-sectional correlation of employment growth, since 1990",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/6f15e9d5407f53bec5affc69b91b3c0e9585ab19be1e1753277e08ad54c89efd.png

Annual changes in unemployment rates #

Volatility of unemployment rate changes have been very different across countries, reflecting differences in labor markets and social security incentives for registration, as well as the general registered level of joblessness.

xcatx = ["UNEMPLRATE_NSA_3MMA_D1M1ML12"]
cidx = list(set(cids_exp))

msp.view_ranges(
    dfx,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="std",
    start=start_date,
    title="Means and standard deviations of unemployment rate changes since 1990",
    xcat_labels=["Annual change in unemployment rates, 3-month moving average"],
    kind="bar",
    size=(16, 8),
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/13dc5054713e4eb1a32272dbc350f6fbc60dc7c4e74150da9754990a3a8336f1.png

Similar to demographic trend indicators, there has been a clear linear relation between average unemployment growth and its standard deviation, across currency areas and years. This suggests high absolute unemployment growth comes with high uncertainty. This inherent uncertainty leads to more pronounced fluctuations in unemployment rates, resulting in a higher standard deviation.

dfa = msp.historic_vol(
    dfx, xcat="UNEMPLRATE_NSA_3MMA_D1M1ML12", cids=cids, lback_meth="xma", postfix="_ASD"
)
dfx = msm.update_df(dfx, dfa)


cr = msp.CategoryRelations(
    dfx,
    xcats=["UNEMPLRATE_NSA_3MMA_D1M1ML12", "UNEMPLRATE_NSA_3MMA_D1M1ML12_ASD"],
    cids=cids_exp,
    freq="A",
    lag=0,
    xcat_aggs=["mean", "mean"],
    xcat_trims=[20, 20],
    start="2000-01-01",
)

cr.reg_scatter(
    title="Unemployment growth and its standard deviations across countries and years since 2000",
    xlab="Change in unemployment rate: over a year ago, 3-month moving average",
    ylab="Average annualized standard deviation of the change in unemployment rate",
    prob_est="map",
    fit_reg=False,
    coef_box=None,
    )
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/59aff8520c0042bae14438ab14336bd26a43f4b29eaa6744ba1e751b02d81816.png
xcatx = ["UNEMPLRATE_NSA_3MMA_D1M1ML12"]
cidx = list(set(cids_exp))

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Unemployment rates, change over a year, 3-month moving average (or quarterly)",
    legend_fontsize=17,
    title_fontsize=27,
    xcat_labels=["3-month moving average"],
    ncol=4,
    same_y=False,
    size=(12, 7),
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/5fd24f8d360040b1c65545c8ffb7699326b07b0198856628edcd95cd1b4445ef.png

Employment growth is strongly, but not perfectly (negatively) correlated with the change in unemployment rate, reflecting the different methodologies of the two metrics as well as the influence of labor participation rates. Using monthly averages, the magnitude of negative correlation has been roughly 60%. Scatterplots reveal a number of outliers.

The same relationship holds for employment growth and unemployment gap, labor market tightness indicator.

easy_labels = {
    "EMPL_NSA_P1M1ML12": "Employment growth: %oya",
    "EMPL_NSA_P1M1ML12_3MMA": "Employment growth: 3mma",
   }


cr_empl= {}
cr_emplgap= {}

empl=["EMPL_NSA_P1M1ML12", "EMPL_NSA_P1M1ML12_3MMA"]

for sig in empl:
    cr_empl[f"cr_{sig}"] = msp.CategoryRelations(
        dfx,
        xcats=[sig, "UNEMPLRATE_NSA_3MMA_D1M1ML12"],
        cids=cids_exp,
        freq="M",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start_date,
        xcat_trims=[80, 80]
    )

for sig in empl:
    cr_emplgap[f"cr_{sig}"] = msp.CategoryRelations(
        dfx,
        xcats=[sig, "UNEMPLRATE_SA_3MMAv5YMA"],
        cids=cids_exp,
        freq="M",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start_date,
        xcat_trims=[50, 80]
    )    

msv.multiple_reg_scatter(
    cat_rels = [cr_empl["cr_"+ key] for key in list(easy_labels.keys())]+[cr_emplgap["cr_"+ key] for key in list(easy_labels.keys())],
    title="Employment growth and unemployment growth/unemployment gap since 2000",
    xlab="Main measure of employment growth: % over a year ago and 3-month moving average",
    ylab="Unemployment rate change: oya, 3mma/ Unemployment gap: 5YMA",
    figsize=(14, 8),
    ncol=2,
    nrow=2,
    prob_est="map",
    coef_box="upper left",
    subplot_titles = [f"{lab} and change in unemployment rate" for lab in easy_labels.values()] + \
                 [f"{lab} and unemployment gap" for lab in easy_labels.values()]
)
EMPL_NSA_P1M1ML12 misses: ['CHF', 'CZK', 'EUR', 'GBP', 'MXN', 'NOK', 'NZD', 'PEN', 'PHP', 'SGD', 'THB', 'ZAR'].
EMPL_NSA_P1M1ML12 misses: ['CHF', 'CZK', 'EUR', 'GBP', 'MXN', 'NOK', 'NZD', 'PEN', 'PHP', 'SGD', 'THB', 'ZAR'].
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/e2ef912a7bc2c6f9e33e7fb66393eed5198728e2e76bada508ea9780bd4d4813.png

Shorter-term changes in unemployment rates #

Seasonally adjusted unemployment changes can be more timely in detecting labor market and broader business cycle trends.

The 6-month-over-6-month changes look like good trend indicators for most countries. The 3-month-over-3-month changes are subject to greater volatility, but often detect large shifts in labor market conditions earlier.

xcatx = ["UNEMPLRATE_SA_D3M3ML3", "UNEMPLRATE_SA_D6M6ML6"]
cidx = list(set(cids_exp))
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start=start_date,
    title="Changes in seasonally-adjusted unemployment rates since 1990",
    xcat_labels=["3 months over previous 3 months", "6 months over previous 6 months"],
    legend_fontsize=17,
    title_fontsize=27,
    ncol=4,
    same_y=False,
    size=(12, 7),
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/4636daa80971717c81e5fa5d779aed61e8b8021e08fa74ac3187dbedd45aeeaa.png

Information states on unemployment trends have been dominantly positively correlated across currency markets.

msp.correl_matrix(dfx, 
                  xcats="UNEMPLRATE_SA_D6M6ML6", 
                  cids=cidx, 
                  size=(20, 14))
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/fa4ab3d55f37cf14f98fb4c2c34d11809cdbe2217b6c785183cf0ec11ec51b76.png

Importance #

Empirical clues #

Employment growth and equity returns #

Employment growth has been strongly and negatively correlated with subsequent equity market performance in past decades. This plausibly reflects that strong labor market dynamics place upward pressure on real interest rates and wage growth. The relationship has prevailed across decades.

Simple employment growth is a powerful economic indicator on its own. However, it can be enhanced as detailed in the notebook Demographic trends , which involves adjusting the employment growth indicator by the workforce growth. This adjusted indicator shows a stronger correlation and significance with subsequent equity returns.

calcs = [
    "XEMPL_NSA_P1M1ML12_3MMA = EMPL_NSA_P1M1ML12_3MMA - WFORCE_NSA_P1Y1YL1_5YMM", 
]

dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_dm, blacklist=None)
dfx = msm.update_df(dfx, dfa)
dfxx = dfx[dfx["xcat"] == "EMPL_NSA_P1M1ML12_3MMA"]
dfxt = dfxx.pivot(index="real_date", columns="cid", values="value")
dfat = dfxt.sub(
    dfxt.median(axis=0), axis=1
)  # excess employment growth through median subtraction
dfa = dfat.unstack().reset_index().rename({0: "value"}, axis="columns")
dfa["xcat"] = "XEMPL_NSA_P1M1ML12_3MMA"
dfx = msm.update_df(dfx, dfa)
xcatx = ["EMPL_NSA_P1M1ML12_3MMA", "XEMPL_NSA_P1M1ML12_3MMA"]

easy_labels = {
    "EMPL_NSA_P1M1ML12_3MMA": "Employment growth trend",
    "XEMPL_NSA_P1M1ML12_3MMA": "Excess employment growth trend",
}

cr_equity = {}

for sig in xcatx:
    cr_equity[f"cr_{sig}"] = msp.CategoryRelations(
        dfx,
        xcats=[sig, "EQXR_NSA"],
        cids=cids_dmeq,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start_date,
        xcat_trims=[None, None],
    )


msv.multiple_reg_scatter(
    cat_rels=[cr_equity["cr_" + key] for key in list(easy_labels.keys())],
    title="Employment growth trends and subsequent equity returns, panel of 8 developed markets, since 2000",
    ylab="Next quarter's equity index future return, %",
    ncol=2,
    nrow=1,
    figsize=(20, 10),
    prob_est="map",
    coef_box="lower left",
    subplot_titles=[lab for lab in list(easy_labels.values())],
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/9204f2710dd26f6c9d1d7a6662dfa834a7eb3463f1bbcf46afe27a76ba9ace7e.png

The negative correlation between employment growth (or excess employment growth) is consistent across almost all developed markets, apart from Japan, where the relationship is positive.

cr_equity["cr_XEMPL_NSA_P1M1ML12_3MMA"].reg_scatter(
    labels=False,
    coef_box="upper left",
    title="Excess employment growth and subsequent equity returns, panel of 8 developed markets since 2000",
    ylab="Next quarter's equity return, %",
    separator="cids",
  )
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/3cb8a451822a7669bb2e6709cfb1cc4ed8da3c4ab3d4b35afdac76292d7a915c.png

Employment growth versus FX benchmarks #

dfb = dfx[dfx["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")
fxblack
{'BRL': (Timestamp('2012-12-03 00:00:00'), Timestamp('2013-09-30 00:00:00')),
 'CHF': (Timestamp('2011-10-03 00:00:00'), Timestamp('2015-01-30 00:00:00')),
 'CNY': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-06-03 00:00:00')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
 'HKD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-06-03 00:00:00')),
 'ILS': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-12-30 00:00:00')),
 'INR': (Timestamp('2000-01-03 00:00:00'), Timestamp('2004-12-31 00:00:00')),
 'MYR_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2007-11-30 00:00:00')),
 'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2024-06-03 00:00:00')),
 'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
 'RON': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2024-06-03 00:00:00')),
 'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-06-03 00:00:00')),
 'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
 'TRY_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2003-09-30 00:00:00')),
 'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2024-06-03 00:00:00'))}

To explore the relationship between labor market dynamics indicators and subsequent FX returns, we calculate relative employment growth as the difference between quantamental indicators of reported employment trends in the reference currency area and the natural benchmark country. The growth trends are measured as % over a year ago in 3-month moving averages. In line with other posts, we calculate differentials to benchmarks for three types of currencies: those trading against USD, EUR and both USD and EUR. The list of currencies is as follows:

  • Currencies traded against EUR: [“CHF”, “CZK”, “HUF”, “NOK”, “PLN”, “RON”, “SEK”]

  • Currencies traded against USD and EUR: [“GBP”, “RUB”, “TRY”]. The benchmark equally weighs USD and EUR data

  • Currencies traded against USD: all other currencies

The new relative indicators receive postfix _vBM

cids_fx = list(set(cids) - set(["EUR", "USD"]))
cids_eur = ["CHF", "CZK", "HUF", "NOK", "PLN", "RON", "SEK"]  # trading against EUR
cids_eud = ["GBP", "RUB", "TRY"]  # trading against EUR and USD
cids_usd = list(set(cids_fx) - set(cids_eur + cids_eud))  # trading against USD



xcatx = ["EMPL_NSA_P1M1ML12_3MMA", "XEMPL_NSA_P1M1ML12_3MMA", "UNEMPLRATE_NSA_3MMA_D1M1ML12"]

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(
        dfx,
        calcs=calc_eur,
        cids=cids_eur,
    )
    dfa_usd = msp.panel_calculator(
        dfx,
        calcs=calc_usd,
        cids=cids_usd,
    )
    dfa_eud = msp.panel_calculator(
        dfx,
        calcs=calc_eud,
        cids=cids_eud,
    )

    dfa = pd.concat([dfa_eur, dfa_usd, dfa_eud])
    dfx = msm.update_df(dfx, dfa)

There is strong evidence of a positive predictive relation between relative labor market dynanmics, defined as relative improvement of the base currency area versus the benchmark (USD or EUR) and subsequent FX forward returns. We observe significant positive correlation between relative employment growth and subsequent fx returns in the next quarter for developed markets since 2000. For emerging markets, this relationship is also positive , but not quite significant, due probably to the lower quality and importance of payroll data in many EM countries. For both emerging and developed markets the relationship between relative unemployment trend and subsequent fx return has been negative and significant.

easyfx_labels = {
    "EMPL_NSA_P1M1ML12_3MMAvBM": "relative employment growth trend (%oya, 3mma)",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM": "relative trend in unemployment rate, (diff oya, 3mma)",
}

xcatx = ["EMPL_NSA_P1M1ML12_3MMAvBM", "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM"]


cr_emfx = {}
cr_dmfx = {}

for sig in xcatx:
    cr_dmfx[f"cr_{sig}"] = msp.CategoryRelations(
        dfx,
        xcats=[sig, "FXXR_NSA"],
        cids=cids_dmfx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        blacklist=fxblack,
        start=start_date,
        xcat_trims=[None, None],
    )


for sig in xcatx:
    cr_emfx[f"cr_{sig}"] = msp.CategoryRelations(
        dfx,
        xcats=[sig, "FXXR_NSA"],
        cids=cids_emfx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        blacklist=fxblack,
        start=start_date,
        xcat_trims=[20, 20],
    )


msv.multiple_reg_scatter(
    cat_rels=[cr_dmfx["cr_" + key] for key in list(easyfx_labels.keys())]
    + [cr_emfx["cr_" + key] for key in list(easyfx_labels.keys())],
    title="Relative labor market dynamics (versus benchmark currency area) and subsequent FX returns, since 2000",
    ylab="Next quarter's FX forward return versus benchmark currency, %",
    ncol=2,
    nrow=2,
    figsize=(15, 10),
    prob_est="map",
    coef_box="lower left",
    subplot_titles=[f"Developed markets: {lab}" for lab in list(easyfx_labels.values())]
    + [f"Emerging markets: {lab}" for lab in list(easyfx_labels.values())],
)
EMPL_NSA_P1M1ML12_3MMAvBM misses: ['IDR', 'INR'].
UNEMPLRATE_NSA_3MMA_D1M1ML12vBM misses: ['IDR', 'INR'].
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/62120b1fa9c7c7c35d02249ff74e5194918ba988c2285f4929f00e04bf488d53.png

To check how well the labor market dynamics signals are predicting returns, we calculate accuracy measures, such as the share of correctly predicted directions of subsequent returns relative to all predictions. It not only shows an important aspect of feature-target co-movement but also implicitly tests if the signal’s neutral (zero) level has been well chosen. A particularly important metric for macro trading strategies is balanced accuracy, which is the average of the proportions of correctly predicted positive and negative returns. The robustness of accuracy metrics, parametric (Pearson) correlation, and non-parametric (Kendall) correlation should be checked across time periods, cross-sections, and variations of features and returns.

# Negative values

xcatx = ["EMPL_NSA_P1M1ML12_3MMA", "EMPL_NSA_P1M1ML12_3MMAvBM"]
calcs = []

for xc in xcatx:
    calcs += [f"{xc}_NEG = - {xc}"]

dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_dmfx)
dfx = msm.update_df(dfx, dfa)

srr = mss.SignalReturnRelations(
    dfx,
    cids=cids_dmfx,
    sigs=["EMPL_NSA_P1M1ML12_3MMAvBM_NEG", "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM"],
    blacklist=fxblack,
    sig_neg=[True, True] ,
    rets="FXXR_NSA",
    freqs=["M", "Q"],
    start=start_date,
)


tbx = (
    srr.multiple_relations_table(signal_name_dict={
        "EMPL_NSA_P1M1ML12_3MMAvBM_NEG_NEG": "Negative relative employment growth signal",
         "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM_NEG": "Relative unemployment trend signal"}, 

         )
    .reset_index(level=["Aggregation", "Return", ], drop=True)
    .reset_index()
)

#tbx.set_index(["Signal", "Frequency"], inplace=True)

# for column modifications
dict_cols = {
    "Signal" : "Signal",
    "Frequency": "Frequency",
    "accuracy": "Accuracy",
    "bal_accuracy": "Balanced accuracy",
    "pos_sigr": "Share of positive signals",
    "pos_retr": "Share of positive returns",
    "pearson": "Pearson coefficient",
    "kendall": "Kendall coefficient",
}

# Filter and rename the columns

tbx = tbx[list(dict_cols.keys())]
tbx.rename(columns=dict_cols, inplace=True)
tbx.set_index(["Signal", "Frequency"], inplace=True)

tbx = tbx.style.format("{:.2f}").set_caption(
        "Predictive accuracy and correlation with respect to subsequent FXXR_NSA").set_table_styles(
       [{"selector": "caption", "props": [("text-align", "center"), ("font-weight", "bold"), ("font-size", "17px")]}])

display(tbx)
Predictive accuracy and correlation with respect to subsequent FXXR_NSA
Accuracy Balanced accuracy Share of positive signals Share of positive returns Pearson coefficient Kendall coefficient
Signal Frequency
Negative relative employment growth signal M 0.52 0.52 0.50 0.51 0.11 0.06
Q 0.53 0.53 0.50 0.52 0.17 0.09
Relative unemployment trend signal M 0.53 0.53 0.43 0.51 0.09 0.06
Q 0.55 0.55 0.43 0.53 0.16 0.10
naive_pnl = msn.NaivePnL(
    dfx,
    ret="FXXR_NSA",
    sigs=["EMPL_NSA_P1M1ML12_3MMAvBM_NEG", "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM"],
    cids=cids_dmfx,
    start="2000-01-01",
 
)

for sig in ["EMPL_NSA_P1M1ML12_3MMAvBM_NEG", "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM"]:
    naive_pnl.make_pnl(
        sig,
        sig_neg=True,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long")

Naive PnL for both employment growth signal and for unemployment trend signal display consistent outperformance in relation of FX-forward position:

naive_pnl.plot_pnls(
    pnl_cats=naive_pnl.pnl_names,
    pnl_cids=["ALL"],
    title="Naive FX-forward PnL of smaller DM currency positions",
    xcat_labels={
    "EMPL_NSA_P1M1ML12_3MMAvBM_NEG_PZN": "Employment growth trend based PnL",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12vBM_PZN": "Unemployment signal based trend",
    "Long": "Long only"}
    )
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/9904447e1414ca68b396ec93f745915aea75c4f67e0e5201679c62e7a54a4460.png

USD excess employment growth versus gold returns #

We compile a separate dataframe containing US labor market dynamics, including previously calculated excess employment trends and gold futures returns. For future calculations, we assign the ticker "GLD" to this US series.

# Pick the commodity returns

filt_cat = dfx["xcat"].isin(["COXR_VT10", "COXR_NSA"])
filt_cid = dfx["cid"].isin(["GLD"])
dfx_gld = dfx[filt_cat & filt_cid]

# Pick the U.S. indicators and assign them to the gold cross-section


xcatx = main + ["XEMPL_NSA_P1M1ML12_3MMA", "XUNEMPLRATE_SA_D6M6ML6"]
filt_cat = dfx["xcat"].isin(xcatx)
filt_cid = dfx["cid"] == "USD"

dfx_usd = dfx[filt_cat & filt_cid]
dfx_usd["cid"] = "GLD"
dfx_gld = msm.update_df(dfx_gld, dfx_usd)

Plausibly, there should ne a negative predictive relation between U.S. employment growth and gold future returns in USD. That is because strong labor market dynamics by themselves bode for increases in real interests, raising opportunity costs of gold holdings. Indeed, excess employment growth has displayed signficant negative predictive power for gold returns in USD, both at a monthly and quarterly frequency since 2000.

cr_gold = msp.CategoryRelations(
        dfx_gld,
        xcats=["XEMPL_NSA_P1M1ML12_3MMA", "COXR_NSA"],
        cids=["GLD"],
        freq="M",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start_date,
        xcat_trims=[None, None]
    )


cr_gold.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Excess employment growth over work force growth, %oya, 3-month average, since 2000",
    ylab="Gold future returnm %, next month",
    title="Excess U.S. employment growth and gold futures returns",
    size=(9, 7),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/themes/economic-trends/_images/91ac30483e37ce980795b602e7fcccacba84ca9d86504a0b7255178e02e730f4.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).