Macroeconomic cycles and asset class returns #

This notebook offers the necessary code to replicate the research findings discussed in Macrosynergy’s post “Macroeconomic cycles and asset class returns” . Its primary objective is to inspire readers to explore and conduct additional investigations while also providing a foundation for testing their own unique ideas.

Get packages and JPMaQS data #

This notebook primarily relies on the standard packages available in the Python data science stack. However, there is an additional package macrosynergy that is required for two purposes:

  • Downloading JPMaQS data: The macrosynergy package facilitates the retrieval of JPMaQS data, which is used in the notebook.

  • For the analysis of quantamental data and value propositions: The macrosynergy package provides functionality for performing quick analyses of quantamental data and exploring value propositions.

For detailed information and a comprehensive understanding of the macrosynergy package and its functionalities, please refer to the “Introduction to Macrosynergy package” on the Macrosynergy Academy or visit the following link on Kaggle .

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 datetime import timedelta, date, datetime
from itertools import combinations
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 .

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

# General cross-sections lists

cids_g3 = ["EUR", "JPY", "USD"]  # DM large currency areas
cids_dmsc = ["AUD", "CAD", "CHF", "GBP", "NOK", "NZD", "SEK"]  # DM small currency areas
cids_latm = ["BRL", "COP", "CLP", "MXN", "PEN"]  # Latam
cids_emea = ["CZK", "HUF", "ILS", "PLN", "RON", "RUB", "TRY", "ZAR"]  # EMEA
cids_emas = ["IDR", "INR", "KRW", "MYR", "PHP", "SGD", "THB", "TWD"]  # EM Asia ex China

cids_dm = cids_g3 + cids_dmsc
cids_em = cids_latm + cids_emea + cids_emas
cids = cids_dm + cids_em

cids_nomp = ["COP", "IDR", "INR"]  # countries that have no employment growth data
cids_mp = list(set(cids) - set(cids_nomp))

# Equity cross-sections lists

cids_dmeq = ["EUR", "JPY", "USD"] + ["AUD", "CAD", "CHF", "GBP", "SEK"]
cids_emeq = ["BRL", "INR", "KRW", "MXN", "MYR", "SGD", "THB", "TRY", "TWD", "ZAR"]
cids_eq = cids_dmeq + cids_emeq

# FX cross-sections lists

cids_nofx = ["EUR", "USD", "SGD"]
cids_fx = list(set(cids) - set(cids_nofx))

cids_dmfx = set(cids_dm).intersection(cids_fx)
cids_emfx = set(cids_em).intersection(cids_fx)

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

# IRS cross-section lists

cids_dmsc_du = ["AUD", "CAD", "CHF", "GBP", "NOK", "NZD", "SEK"]
cids_latm_du = ["CLP", "COP", "MXN"]  # Latam
cids_emea_du = [
    "CZK",
    "HUF",
    "ILS",
    "PLN",
    "RON",
    "RUB",
    "TRY",
    "ZAR",
]  # EMEA
cids_emas_du = ["CNY", "HKD", "IDR", "INR", "KRW", "MYR", "SGD", "THB", "TWD"]

cids_dmdu = cids_g3 + cids_dmsc_du
cids_emdu = cids_latm_du + cids_emea_du + cids_emas_du
cids_du = cids_dmdu + cids_emdu

JPMaQS indicators are conveniently grouped into 6 main categories: Economic Trends, Macroeconomic balance sheets, Financial conditions, Shocks and risk measures, Stylyzed trading factors, and Generic returns. Each indicator has a separate page with notes, description, availability, statistical measures, and timelines for main currencies. The description of each JPMaQS category is available either under Macro Quantamental Academy , JPMorgan Markets (password protected). In particular, the indicators used in this notebook could be found under Labor market dynamics , Demographic trends , Consumer price inflation trends , Intuitive growth estimates , Long-term GDP growth , Private credit expansion , Equity index future returns , FX forward returns , and Duration returns .

# Category tickers

main = [
    "EMPL_NSA_P1M1ML12_3MMA",
    "EMPL_NSA_P1Q1QL4",
    "WFORCE_NSA_P1Y1YL1_5YMM",
    "WFORCE_NSA_P1Q1QL4_20QMM",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12",
    "UNEMPLRATE_NSA_D1Q1QL4",
    "UNEMPLRATE_SA_D1Q1QL4",  # potentially NZD only
    "UNEMPLRATE_SA_D3M3ML3",
    "UNEMPLRATE_SA_D1Q1QL1",
    "UNEMPLRATE_SA_3MMA",
    "UNEMPLRATE_SA_3MMAv10YMM",
    "CPIH_SA_P1M1ML12",
    "CPIH_SJA_P6M6ML6AR",
    "CPIC_SA_P1M1ML12",
    "CPIC_SJA_P6M6ML6AR",
    "INFTEFF_NSA",
    "INTRGDPv5Y_NSA_P1M1ML12_3MMA",
    "RGDP_SA_P1Q1QL4_20QMM",
    "PCREDITBN_SJA_P1M1ML12",
]
xtra = ["GB10YXR_NSA"]

rets = [
    "EQXR_NSA",
    "EQXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
    "FXXR_NSA",
    "FXXR_VT10",
    "FXXRHvGDRB_NSA",
    "DU02YXR_NSA",
    "DU02YXR_VT10",
    "DU05YXR_VT10",
]

xcats = main + rets + xtra
# 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=start_date,
        suppress_warning=True,
        metrics=["value"],
        report_time_taken=True,
        show_progress=True,
    )
Maximum number of tickers is 930
Downloading data from JPMaQS.
Timestamp UTC:  2025-10-02 13:33:45
Connection successful!
Requesting data: 100%|██████████| 47/47 [00:09<00:00,  4.74it/s]
Downloading data: 100%|██████████| 47/47 [00:23<00:00,  2.04it/s]
Time taken to download data: 	34.58 seconds.
Some expressions are missing from the downloaded data. Check logger output for complete list.
229 out of 930 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()`.
display(df["xcat"].unique())
display(df["cid"].unique())
df["ticker"] = df["cid"] + "_" + df["xcat"]
df.head(3)
array(['DU05YXR_VT10', 'EQXR_VT10', 'CPIH_SJA_P6M6ML6AR', 'INFTEFF_NSA',
       'DU02YXR_VT10', 'FXXR_NSA', 'PCREDITBN_SJA_P1M1ML12', 'EQXR_NSA',
       'FXTARGETED_NSA', 'DU02YXR_NSA', 'FXXRHvGDRB_NSA',
       'CPIH_SA_P1M1ML12', 'FXUNTRADABLE_NSA', 'EMPL_NSA_P1M1ML12_3MMA',
       'INTRGDPv5Y_NSA_P1M1ML12_3MMA', 'CPIC_SA_P1M1ML12', 'GB10YXR_NSA',
       'CPIC_SJA_P6M6ML6AR', 'FXXR_VT10', 'UNEMPLRATE_NSA_3MMA_D1M1ML12',
       'WFORCE_NSA_P1Y1YL1_5YMM', 'RGDP_SA_P1Q1QL4_20QMM',
       'UNEMPLRATE_SA_D3M3ML3', 'UNEMPLRATE_SA_3MMAv10YMM',
       'UNEMPLRATE_SA_3MMA', 'EMPL_NSA_P1Q1QL4',
       'WFORCE_NSA_P1Q1QL4_20QMM', 'UNEMPLRATE_NSA_D1Q1QL4',
       'UNEMPLRATE_SA_D1Q1QL1'], dtype=object)
array(['AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'COP', 'CZK', 'EUR', 'GBP',
       'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK',
       'NZD', 'PEN', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB',
       'TRY', 'TWD', 'USD', 'ZAR'], dtype=object)
real_date cid xcat value ticker
0 2001-07-04 AUD DU05YXR_VT10 -0.233174 AUD_DU05YXR_VT10
1 2001-07-05 AUD DU05YXR_VT10 0.651543 AUD_DU05YXR_VT10
2 2001-07-06 AUD DU05YXR_VT10 0.012344 AUD_DU05YXR_VT10
scols = ["cid", "xcat", "real_date", "value"]  # required columns
dfx = df[scols].copy()
dfx.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4458398 entries, 0 to 4458397
Data columns (total 4 columns):
 #   Column     Dtype         
---  ------     -----         
 0   cid        object        
 1   xcat       object        
 2   real_date  datetime64[ns]
 3   value      float64       
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 136.1+ MB

Blacklist dictionaries #

Identifying and isolating periods of official exchange rate targets, illiquidity, or convertibility-related distortions in FX markets is the first step in creating an FX trading strategy. These periods can significantly impact the behavior and dynamics of currency markets, and failing to account for them can lead to inaccurate or misleading findings.

dfb = df[df["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')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 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('2025-10-01 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('2025-10-01 00:00:00')),
 'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2025-10-01 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-07-31 00:00:00'))}
dublack = {
    "TRY": fxblack["TRY_2"]
}  # create a customized blacklist for TRY to be used later in the code

Availability #

It is important to assess data availability before conducting any analysis. It allows to identify any potential gaps or limitations in the dataset, which can impact the validity and reliability of analysis and ensure that a sufficient number of observations for each selected category and cross-section is available as well as determining the appropriate time periods for analysis.

msm.check_availability(df, xcats=main, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/566be33e918002b706c93d5fd66a9a5c4581a5c3f50e540927a13a071875ce25.png

Transformations and checks #

Features #

Name replacements #

dict_repl = {
    "EMPL_NSA_P1Q1QL4": "EMPL_NSA_P1M1ML12_3MMA",
    "WFORCE_NSA_P1Q1QL4_20QMM": "WFORCE_NSA_P1Y1YL1_5YMM",
    "UNEMPLRATE_NSA_D1Q1QL4": "UNEMPLRATE_NSA_3MMA_D1M1ML12",
    "UNEMPLRATE_SA_D1Q1QL1": "UNEMPLRATE_SA_D3M3ML3",
}

for key, value in dict_repl.items():
    dfx["xcat"] = dfx["xcat"].str.replace(key, value)
msm.check_availability(dfx, xcats=list(dict_repl.values()), cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/7f9bfbb59147c575da5db706c4bea214a4774a06d906d1951f64bb515ae4c11e.png

Labor market scores #

Excess employment growth #

To proxy the impact of the business cycle state on employment growth, a common approach is to calculate the difference between employment growth and the long-term median of workforce growth. This difference is often referred to as “excess employment growth.” By calculating excess employment growth, one can estimate the component of employment growth that is attributable to the business cycle state. This measure helps to identify deviations from the long-term trend and provides insights into the cyclical nature of employment dynamics.

calcs = ["XEMPL_NSA_P1M1ML12_3MMA = EMPL_NSA_P1M1ML12_3MMA - WFORCE_NSA_P1Y1YL1_5YMM "]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids, blacklist=None)
dfx = msm.update_df(dfx, dfa)

The macrosynergy package provides two useful functions, view_ranges() and view_timelines() , which facilitate the convenient visualization of data for selected indicators and cross-sections. These functions assist in plotting means, standard deviations, and time series of the chosen indicators.

xcatx = ["EMPL_NSA_P1M1ML12_3MMA", "WFORCE_NSA_P1Y1YL1_5YMM"]
cidx = cids_mp

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,

)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/1c64e46f8a035b19fa6e2e55605d78252b8e87889455010a0fafde9ff3acd100.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/452dfdf4604cee6bf9189f434e1fb0a2743cf08ca82b929d9015978952a92267.png
xcatx = ["EMPL_NSA_P1M1ML12_3MMA", "XEMPL_NSA_P1M1ML12_3MMA"]
cidx = cids_mp

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
   )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/8ca93177ba8b30b9d0b65182ab4f3bd3704701f207af5bb3ec0bec2f1f9acc7c.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/2f0e636d88162c185842ada37e7c27ef4869c0f8da233c96a1110e9cbaa38b43.png

Unemployment rates and gaps #

Unemployment rates and unemployment gaps are commonly used measures in labor market analysis. The unemployment rate is a widely used indicator that measures the percentage of the labor force that is unemployed and actively seeking employment. The unemployment gap refers to the difference between the actual unemployment rate and a reference or target unemployment rate. The unemployment gap is used to assess the deviation of the current unemployment rate from the desired or expected level. Here we compare the standard unemployment rate, sa, 3mma with unemployment rate difference, 3-month moving average minus the 10-year moving median. Comparison between the two can give insights into the short-term fluctuations and the long-term trend of the unemployment rate.

xcatx = ["UNEMPLRATE_SA_3MMA", "UNEMPLRATE_SA_3MMAv10YMM"]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/22ab4ac32c8331045982f58562f324b0789f9998508d565b091e7e559fddf0a7.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/6e0b8b35ab2e89f00a1614e9d4130214ae42440e49a1dfc41e7422e8f04f102b.png

Unemployment changes #

We create a simple average of two unemployment growth indicators: unemploent rate change and unemployment growth:

calcs = [
    "UNEMPLRATE_DA = 1/2 * ( UNEMPLRATE_NSA_3MMA_D1M1ML12 + UNEMPLRATE_SA_D3M3ML3 )",
]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids, blacklist=None)
dfx = msm.update_df(dfx, dfa)
xcatx = ["UNEMPLRATE_NSA_3MMA_D1M1ML12", "UNEMPLRATE_SA_D3M3ML3", "UNEMPLRATE_DA"]
cidx = cids

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
 
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/6d3261f5ef23f6c6759af52a4844bd62e231ce39dd98b3ec99ef669bd03510e7.png

Labor tightening scores #

We compute two types of labor market z-scores. One is based on the panel and assumes no structural differences in the features quantitative effects across sections. The other is half based on cross-section alone, which implies persistent structural differences in distributions and their impact on targets. For a description and possible options of function make_zn_scores() please see either Kaggle or under Academy notebooks .

xcat_lab = [
    "XEMPL_NSA_P1M1ML12_3MMA",
    "UNEMPLRATE_DA",
    "UNEMPLRATE_SA_3MMAv10YMM",
]
cidx = msm.common_cids(dfx, xcat_lab)

pws = [0.25, 1]  # cross-sectional and panel-based normalization

for xc in xcat_lab:
    for pw in pws:
        dfa = msp.make_zn_scores(
            dfx,
            xcat=xc,
            cids=cidx,
            sequential=True,
            min_obs=522,  # oos scaling after 2 years of panel data
            est_freq="m",
            neutral="zero",
            pan_weight=pw,
            thresh=3,
            postfix="_ZNP" if pw == 1 else "_ZNM",
        )
        dfx = msm.update_df(dfx, dfa)

The individual category scores are combined into a single labor market tightness score.

xcatx = [
    "XEMPL_NSA_P1M1ML12_3MMA",
    "UNEMPLRATE_DA",
    "UNEMPLRATE_SA_3MMAv10YMM",
]
cidx = msm.common_cids(dfx, xcat_lab)
# cidx.remove("NZD")  # ISSUE: invalid empty series created above
n = len(xcatx)
wx = [1 / n] * n
sx = [1, -1, -1]  # signs for tightening


dix = {"ZNP": [xc + "_ZNP" for xc in xcatx], "ZNM": [xc + "_ZNM" for xc in xcatx]}

dfa = pd.DataFrame(columns=dfx.columns).reindex([])
for key, value in dix.items():
    dfaa = msp.linear_composite(
        dfx,
        xcats=value,
        weights=wx,
        signs=sx,
        cids=cidx,
        complete_xcats=False,  # if some categories are missing the score is based on the remaining
        new_xcat="LABTIGHT_" + key,
    )
    dfa = msm.update_df(dfa, dfaa)
dfx = msm.update_df(dfx, dfa)
xcatx = [xc + "_ZNP" for xc in xcat_lab]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/1804da2d8d3c6f6e47895cd85ed9642753d7bdb60a375112226e8fb414f4c98a.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/55020405e59eaf9c5bc6a823742128f44c7a7f9f5f1b6c61fde5c4632ad2f42e.png

To summarize: we created two Labor market tightening indicators: These are a composite of three quantamental indicators that are jointly tracking the usage of the economy’s labor force. The first is employment growth relative to workforce growth, where the former is measured in % over a year ago and 3-month average and the latter is an estimate based on the latest available 5 years of workforce growth. The second sub-indicator measures changes in the unemployment rate over a year ago and over the last three months, both as a 3-month moving average (view documentation here). The third labor market indicator is the level of the unemployment rate versus a 10-year moving median, again as a 3-month moving average. All three indicators are z-scored, then combined with equal weights, and then the combination is again z-scored for subsequent analysis and aggregation. The difference between the two is the difference in the importance of the panel versus the individual cross-sections for scaling the zn-scores. “_ZNP” indicator uses the whole panel data as the basis for the parameters and “_ZNM” uses 1/4 of the whole panel and 3/4 of an individual cross-section.

xcatx = ["LABTIGHT_ZNP", "LABTIGHT_ZNM"]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
  )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/ce04048c6449cbac4731a9979c3d72f7f14aa25614013d9c30e672bb20d440ec.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/05d6ea11b5845e21709f0ef4849424cd928dfce83ff7d3c9bcf71243d0bb5053.png

Excess inflation #

Similarly to labor market tightness, we can calculate plausible metrics of excess inflation versus a country’s effective inflation target. To make the targets comparable across markets, the relative target deviations need denominator bases that should never be less than 2, so we clip the Estimated official inflation target for next year at a minimum value of 2 and use it as denominator. We then calculate absolute and relative target deviations for a range of CPI inflation metrics.

dfa = msp.panel_calculator(
    dfx,
    ["INFTEBASIS = INFTEFF_NSA.clip(lower=2)"],
    cids=cids,
)
dfx = msm.update_df(dfx, dfa)
infs = [
    "CPIH_SA_P1M1ML12",
    "CPIH_SJA_P6M6ML6AR",
    "CPIC_SA_P1M1ML12",
    "CPIC_SJA_P6M6ML6AR",
]

for inf in infs:
    calc_iet = f"{inf}vIETR = ( {inf} - INFTEFF_NSA ) / INFTEBASIS"
    dfa = msp.panel_calculator(dfx, calcs=[calc_iet], cids=cids)
    dfx = msm.update_df(dfx, dfa)
xcatx = [inf + "vIETR" for inf in infs]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="box",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
      size=(12, 12),
    all_xticks=True,
  )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/740e8a41402b32f794fd50f90d71f10b79fa8ea5875874e3250ed688ab4c2747.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/113ece35cc166c23cc8104f52bfc3a9410a59e432adcb1ebd18d01a072f6fdbf.png

The individual excess inflation metrics are similar in size and, hence can be directly combined into a composite excess inflation metric.

xcatx = [inf + "vIETR" for inf in infs]
cidx = cids

dfa = msp.linear_composite(
    dfx,
    xcats=xcatx,
    cids=cidx,
    complete_xcats=False,  # if some categories are missing the score is based on the remaining
    new_xcat="CPI_PCHvIETR",
)

dfx = msm.update_df(dfx, dfa)

As before, we normalize values for the composite excess inflation metric around zero based on the whole panel.

xcatx = "CPI_PCHvIETR"
cidx = cids

dfa = msp.make_zn_scores(
    dfx,
    xcat=xcatx,
    cids=cidx,
    sequential=True,
    min_obs=522,  # oos scaling after 2 years of panel data
    est_freq="m",
    neutral="zero",
    pan_weight=1,
    thresh=2.5,
    postfix="_ZNP",
)
dfx = msm.update_df(dfx, dfa)
xcatx = ["CPI_PCHvIETR", "CPI_PCHvIETR_ZNP"]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="box",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=5,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
 )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/ec9d0e265282c0fac50fda88aea8576a66cf50a4b79c8c317d3265979c61d2fe.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/f85e150ec48b3ba6f68c9fd69f364eff14d9d257216cb3448caf900263bb3e9e.png

Excess growth #

Excess real-time growth estimates are z-scored for intuitive interpretation and to winsorize large outliers, which often reflect temporary disruptions and data issues. JPMaQS offers a ready-made indicator of excess estimated GDP growth trend, labelled INTRGDPv5Y_NSA_P1M1ML12_3MMA . For each day this is the latest estimated GDP growth trend (% over a year ago, 3-month moving average) minus a 5-year median of that country’s actual GDP growth rate. The historic median represents the growth rate that businesses and markets have grown used to. The GDP growth trend is estimated based on actual national accounts and monthly activity data, based on sets of regressions that replicate conventional charting methods in markets (view full documentation here). For subsequent aggregation and analysis, we then z-score the indicator (normalize volatility) around its zero value on an expanding out-of-sample basis using all cross sections for estimating the standard deviations. As before, we normalize values for the indicator around zero based on the whole panel.

xcatx = "INTRGDPv5Y_NSA_P1M1ML12_3MMA"
cidx = cids

dfa = msp.make_zn_scores(
    dfx,
    xcat=xcatx,
    cids=cidx,
    sequential=True,
    min_obs=522,  # oos scaling after 2 years of panel data
    est_freq="m",
    neutral="zero",
    pan_weight=1,
    #  thresh=3,
    postfix="_ZNP",
)
dfx = msm.update_df(dfx, dfa)
xcatx = ["INTRGDPv5Y_NSA_P1M1ML12_3MMA", "INTRGDPv5Y_NSA_P1M1ML12_3MMA_ZNP"]
cidx = cids

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="box",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
 
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/4f372ef1ef0ae6c9dc9ff4c755afdfbbcb91316568feb71a3fbaa0d4bca63a71.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/ae7c21099542a9f9d3da5b2cfd0290cad3438101655c5da50c0c568b2b491f0c.png

Features relative to the base currency #

cycles = [
    "LABTIGHT",
    "CPI_PCHvIETR",
    "INTRGDPv5Y_NSA_P1M1ML12_3MMA",
]
xcatx = [cc + "_ZNP" for cc in cycles]

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 + ["SGD"])
    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)
xcatx = ["LABTIGHT_ZNP", "LABTIGHT_ZNPvBM"]
cidx = cids_fx

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    ylab="% daily rate",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
 
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/76d672ee2b40a05d3a14a9da5d360085786b82f2f30a37d79c01529d3f28762b.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/7cc9a42852535aeea0b250fb9d1149f1e7392d6fad1e3758a478ade7181bb1f9.png

Composite z-scores #

We calculate composite zn-scores of cyclical strength with and without labor market tightness. We also calculate composite zn-score differences to FX base currencies with and without labor market tightness.

# Cyclical strength constituents and list of its keys

d_cs = {
    "G": "INTRGDPv5Y_NSA_P1M1ML12_3MMA",
    "I": "CPI_PCHvIETR",
    "L": "LABTIGHT",
    # "C": "XPCREDITBN_SJA_P1M1ML12",  not so relevant for cyclical strength
}
cs_keys = list(d_cs.keys())


# Available cross-sections

xcatx_znp = [d_cs[i] + "_ZNP" for i in cs_keys]
cidx_znp = msm.common_cids(dfx, xcatx_znp)

xcatx_vbm = [d_cs[i] + "_ZNPvBM" for i in cs_keys]
cidx_vbm = msm.common_cids(dfx, xcatx_vbm)

d_ar = {"_ZNP": cidx_znp, "_ZNPvBM": cidx_vbm}


# Collect all cycle strength key combinations

cs_combs = [combo for r in range(1, 5) for combo in combinations(cs_keys, r)]


# Use key combinations to calculate all possible factor combinations

dfa = pd.DataFrame(columns=dfx.columns).reindex([])

for cs in cs_combs:
    for key, value in d_ar.items():
        xcatx = [
            d_cs[i] + key for i in cs
        ]  # extract absolute or relative xcat combination
        dfaa = msp.linear_composite(
            dfx,
            xcats=xcatx,
            cids=value,
            complete_xcats=False,  # if some categories are missing the score is based on the remaining
            new_xcat="CS" + "".join(cs) + key[4:] + "_ZC",
        )
        dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)


# Collect factor combinations in lists

cs_all = dfa["xcat"].unique()
cs_dir = [cs for cs in cs_all if "vBM" not in cs]
cs_rel = [cs for cs in cs_all if "vBM" in cs]
xcatx = ["CSG_ZC"]
cidx = cidx_znp

msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=5,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="Excess GDP growth z-scores",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/0ce81babbe38dc47ad16bda67691ce12342fa205d9fa00dde33cfc52808fbea9.png
xcatx = ["CSL_ZC"]
cidx = cidx_znp

msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=5,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="Labor market tightness composite z-scores",
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/7c681e687a4317f207d3f23c2c444cfb540f87e3799a3c87ad4c6e5433632082.png
xcatx = ["CSI_ZC"]
cidx = cidx_znp

msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    cids=cidx,
    ncol=5,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
     size=(12, 12),
    all_xticks=True,
    title="Excess CPI inflation z-scores",
  )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/8fd2b2d2ebfbd8024543443a2b51033803f6a2370c967a57a60a4a13d94e8127.png
xcatx = ["CSGIL_ZC", "CSGILvBM_ZC"]
cidx = cidx_znp

msp.view_timelines(
    dfx,
    xcats=xcatx[0:2],
    xcat_labels=["outright score", "relative to benchmark currency"],
    cids=cidx,
    ncol=5,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="Composite cyclical strength scores, outright and versus benchmark currency area",
   )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/2e5104345bab5f895d62f67c5d29bcbf88817fcec8949225d6059f5ba6ccaeea.png

Targets #

Directional vol-targeted IRS returns #

xcatx = ["DU02YXR_VT10", "DU05YXR_VT10"]
cidx = list(set(cids_du) - set(["TRY"]))


msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="box",
    sort_cids_by="std",
    ylab="% daily rate",
    start="2000-01-01",
)

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
  )
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/22ede6f0e60d7b572e09c76ea826a4135b1601f2c65f1fe3cf110fd689f57869.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/5e1c8a67813ca2d0797ffba642b9a4892e4f9b13abb3ba32f0a8d448550f66ac.png

Directional equity returns #

xcatx = ["EQXR_NSA", "EQXR_VT10"]
cidx = cids_eq

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="box",
    sort_cids_by="std",
    ylab="% daily rate",
    start="2000-01-01",
)

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
 
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/84e22b018c1171f740bb032c07495049cdd7b93114fcc6ed5cb58f28596aa966.png https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/d5e01158afe7aa2252949cc5e5704b49b42cca64bfd465730c55121f17103c7b.png

FX returns relative to base currencies #

xcatx = ["FXXR_NSA", "FXXR_VT10"]
cidx = cids_fx

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/be98c415bb47c1f1dd11e4a3703c96478dc5de7cc59e84f84f0e5f06e10ca3c3.png

FX versus equity returns #

cidx_fxeq = msm.common_cids(dfx, ["FXXR_VT10", "EQXR_VT10"])
calcs = ["FXvEQXR = FXXR_VT10 - EQXR_VT10 "]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cidx_fxeq, blacklist=None)
dfx = msm.update_df(dfx, dfa)
xcatx = ["FXvEQXR"]
cidx = cidx_fxeq

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/81017c64c3afca85262cc923ca687dddfff5288ad2ce3d8929176c1700d1bcc8.png

FX versus IRS returns #

cidx_fxdu = list(
    set(msm.common_cids(dfx, ["FXXR_VT10", "DU05YXR_VT10"])) - set(["IDR"])
)
calcs = ["FXvDU05XR = FXXR_VT10 - DU05YXR_VT10 "]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cidx_fxdu, blacklist=dublack)
dfx = msm.update_df(dfx, dfa)
xcatx = ["FXvDU05XR"]
cidx = cidx_fxdu

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
 
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/9648fd3b621b023216850e3886ecf9fa742d13a7d8845f0bb40e16ec20c0fa94.png

2s-5s flattener returns #

cidx_du52 = list(
    set(msm.common_cids(dfx, ["DU02YXR_VT10", "DU05YXR_VT10"])) - set(["IDR"])
)
calcs = ["DU05v02XR = DU05YXR_VT10 - DU02YXR_VT10 "]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cidx_du52, blacklist=dublack)
dfx = msm.update_df(dfx, dfa)
xcatx = ["DU05v02XR"]
cidx = cidx_du52

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
  
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/3a4556392a7fa78b617c78923c4bb1042861a9ed5bb7f2afa070e2b9219972dd.png

Value checks #

Directional equity strategy #

Specs and panel test #

sigs = cs_dir
ms = "CSGIL_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "EQXR_VT10"
cidx = msm.common_cids(dfx, sigs + [targ])
# cidx = list(set(cids_dm) & set(cidx))   # for DM alone

dict_eqdi = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_eqdi

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",  # quarterly frequency allows for policy inertia
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score, end of quarter",
    ylab="Equity index future return next quarter for 10% vol target",
    title="Cyclical strength and subsequent equity index futures returns",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/e3efa8a13bb0062692fbc4939ae2a4eb9eea0bff4a8652789f085dc0851ea32d.png

Accuracy and correlation check #

dix = dict_eqdi

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    sig_neg=[True] + [True] * len(rivs),
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_eqdi
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGIL_ZC_NEG/last => EQXR_VT10 0.524 0.522 0.514 0.586 0.607 0.437 0.098 0.000 0.051 0.000 0.522
Mean years 0.524 0.513 0.512 0.583 0.594 0.432 0.043 0.439 0.024 0.442 0.508
Positive ratio 0.538 0.654 0.538 0.692 0.808 0.308 0.615 0.385 0.577 0.346 0.654
Mean cids 0.524 0.520 0.516 0.582 0.603 0.438 0.102 0.202 0.050 0.311 0.521
Positive ratio 0.706 0.706 0.529 1.000 1.000 0.059 0.941 0.824 0.824 0.706 0.706
dix = dict_eqdi
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_VT10 CSGIL_ZC_NEG M last 0.524 0.522 0.514 0.586 0.607 0.437 0.098 0.000 0.051 0.000 0.522
CSGI_ZC_NEG M last 0.534 0.523 0.563 0.586 0.606 0.440 0.087 0.000 0.047 0.000 0.523
CSGL_ZC_NEG M last 0.492 0.500 0.453 0.586 0.586 0.415 0.074 0.000 0.031 0.002 0.500
CSG_ZC_NEG M last 0.517 0.510 0.541 0.586 0.595 0.425 0.051 0.001 0.015 0.137 0.510
CSIL_ZC_NEG M last 0.526 0.525 0.504 0.586 0.611 0.440 0.102 0.000 0.058 0.000 0.526
CSI_ZC_NEG M last 0.538 0.528 0.560 0.588 0.613 0.444 0.081 0.000 0.045 0.000 0.529
CSL_ZC_NEG M last 0.494 0.511 0.402 0.586 0.600 0.423 0.077 0.000 0.043 0.000 0.511
dix = dict_eqdi
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    title="Accuracy of monthly predictions of FX forward returns for 26 EM and DM currencies",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/d21e6a435583aea8d0339e38c7eeda145fbbcaffdd038a6aaf6c04fc03a5612b.png

Naive PnL #

dix = dict_eqdi

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=True,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=True,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_eqdi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/c4c414053083575da75ae0dd0d39bd55bdc61ede6d0d4b7849d1e1bc352dea2f.png
dix = dict_eqdi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGIL_ZC_PZN": "based on negative of cyclical strength z-score",
            "Long only": "long only portfolio across 18 currencies (risk parity)"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="Equity index future PnL across 18 markets",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/0fdfeb9014021edd8cc0b207e0c2074688b55926821d713d7e481f38432fe46b.png
dix = dict_eqdi

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/c06c7fdaa7bb38eeb5cc33fd1381091a3ba716bda3025b727ee5d431101f2fee.png
dix = dict_eqdi

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
CSGIL_ZC_PZN 6.905897 10.0 0.69059 1.053349 -16.292546 -15.115598 -21.246892 0.952152 0.021206 309
CSGL_ZC_PZN 4.918206 10.0 0.491821 0.756866 -16.270477 -20.308411 -46.33157 1.306266 0.09617 309
CSL_ZC_PZN 4.989808 10.0 0.498981 0.750435 -16.090632 -15.876642 -33.076906 1.076208 -0.161347 309
CSIL_ZC_PZN 6.911625 10.0 0.691163 1.024119 -19.958052 -22.205445 -35.429216 0.875111 -0.148668 309
CSI_ZC_PZN 5.906625 10.0 0.590662 0.850266 -19.94683 -23.434328 -36.618201 0.929664 -0.096022 309
CSGI_ZC_PZN 6.510744 10.0 0.651074 0.975025 -16.98606 -16.449276 -24.018757 0.967988 0.106871 309
CSG_ZC_PZN 3.626276 10.0 0.362628 0.551245 -16.07472 -27.496452 -54.810333 1.715043 0.254211 309
CSGIL_ZC_BIN 5.424233 10.0 0.542423 0.788591 -12.016577 -16.284173 -30.343562 0.899223 -0.018881 309
CSGL_ZC_BIN 0.402071 10.0 0.040207 0.058551 -11.971994 -20.705076 -69.502212 11.404664 0.005551 309
CSL_ZC_BIN 2.993753 10.0 0.299375 0.449686 -13.009855 -18.304534 -50.871627 1.558449 -0.212813 309
CSIL_ZC_BIN 5.852657 10.0 0.585266 0.84136 -13.200428 -19.854434 -34.495359 0.83469 -0.101812 309
CSI_ZC_BIN 6.544986 10.0 0.654499 0.922011 -21.877364 -15.617163 -31.563761 0.72367 0.004819 309
CSGI_ZC_BIN 5.900969 10.0 0.590097 0.841982 -15.299395 -19.009972 -27.8634 0.794183 0.058707 309
CSG_ZC_BIN 2.154968 10.0 0.215497 0.309569 -22.802273 -23.262174 -46.740067 2.033631 0.189063 309
dix = dict_eqdi
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="q", start="2000-01-01", figsize=(16, 5)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/3bef771b95daaf03efd17adc84e7348d7e5967b81fe80d52de613253f4c77a68.png

Directional FX strategy #

Specs and panel test #

sigs = cs_rel
ms = "CSGILvBM_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "FXXR_VT10"
cidx = msm.common_cids(dfx, sigs + [targ])
# cidx = list(set(cids_dm) & set(cidx))   # for DM alone

dict_fxdi = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": None,
    "srr": None,
    "pnls": None,
}
dix = dict_fxdi

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",  # quarterly frequency allows for policy inertia
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    end="2023-05-01",
    blacklist=blax,
    xcat_trims=[1000, 40],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score versus benchmark currency area, end of quarter",
    ylab="1-month FX foward return next quarter for 10% vol target",
    title="Relative cyclical strength and subsequent FX forward returns, 2000-2023 (Apr)",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/ba35a482f406ee73e00c438ff9eaedf85c58d8cd438cb5183c037938010b2f9f.png

Accuracy and correlation check #

dix = dict_fxdi

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_fxdi
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGILvBM_ZC/last => FXXR_VT10 0.522 0.526 0.457 0.545 0.573 0.479 0.075 0.000 0.051 0.000 0.526
Mean years 0.522 0.518 0.455 0.545 0.564 0.472 0.063 0.322 0.038 0.303 0.516
Positive ratio 0.692 0.808 0.423 0.692 0.769 0.385 0.846 0.615 0.846 0.615 0.808
Mean cids 0.522 0.522 0.460 0.546 0.570 0.473 0.071 0.310 0.044 0.343 0.521
Positive ratio 0.741 0.704 0.370 0.852 0.926 0.333 0.778 0.667 0.815 0.593 0.704
dix = dict_fxdi
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
FXXR_VT10 CSGILvBM_ZC M last 0.522 0.526 0.457 0.545 0.573 0.479 0.075 0.000 0.051 0.000 0.526
CSGIvBM_ZC M last 0.514 0.517 0.468 0.543 0.562 0.472 0.052 0.000 0.034 0.000 0.517
CSGLvBM_ZC M last 0.518 0.520 0.475 0.545 0.566 0.474 0.063 0.000 0.047 0.000 0.520
CSGvBM_ZC M last 0.514 0.515 0.487 0.539 0.555 0.476 0.028 0.011 0.020 0.008 0.515
CSILvBM_ZC M last 0.523 0.526 0.468 0.544 0.571 0.480 0.081 0.000 0.055 0.000 0.526
CSIvBM_ZC M last 0.514 0.516 0.470 0.541 0.559 0.474 0.051 0.000 0.033 0.000 0.516
CSLvBM_ZC M last 0.525 0.527 0.471 0.543 0.572 0.483 0.075 0.000 0.056 0.000 0.527
dix = dict_fxdi
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/9b6e38899b774f36f0b5e683ee8adebb11565cd560b97ecfd994a13589aa9c3f.png

Naive PnL #

dix = dict_fxdi

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_fxdi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/48321811a97435bf30fc6179a44c585d295eb38311a55b8287ce9f9533692841.png
dix = dict_fxdi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGILvBM_ZC_PZN":"based on relative cyclical strength z-score",
"Long only": "long only portfolio in all 27 smaller currencies (versus USD and EUR, risk parity)"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="FX forward PnL across 27 currency areas (ex USD and EUR)",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/6f6daa0e7bb4d1a30ee50d9ef70633af4f911446b314cb145e5b7e98c7d76b81.png
dix = dict_fxdi

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/2e4dd7658886b2acbe2a885e01b17a6dd1a711e44eba1511998dd76a947f1ae9.png
dix = dict_fxdi

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_PZN 9.466854 10.0 0.946685 1.413063 -15.176952 -29.556839 -36.862838 0.576782 0.068381 309
CSGIvBM_ZC_PZN 7.754476 10.0 0.775448 1.159728 -10.489763 -22.709241 -38.883142 0.734045 0.060988 309
CSIvBM_ZC_PZN 6.655565 10.0 0.665557 0.986367 -14.542207 -25.378621 -39.378387 0.791491 0.123044 309
CSGvBM_ZC_PZN 4.752109 10.0 0.475211 0.69345 -14.362035 -24.005184 -34.3911 1.087133 -0.047204 309
CSLvBM_ZC_PZN 7.988725 10.0 0.798872 1.1634 -22.321814 -40.172477 -47.598443 0.69758 0.061961 309
CSGLvBM_ZC_PZN 8.272448 10.0 0.827245 1.22761 -15.513887 -30.63893 -42.83372 0.689633 0.01538 309
CSILvBM_ZC_PZN 8.991063 10.0 0.899106 1.332779 -18.339832 -32.823994 -47.005767 0.61916 0.106866 309
CSGILvBM_ZC_BIN 8.288333 10.0 0.828833 1.247246 -11.192794 -22.98809 -38.568993 0.648665 0.024266 309
CSGIvBM_ZC_BIN 3.810782 10.0 0.381078 0.556823 -11.887367 -24.790087 -34.569794 1.231326 0.055529 309
CSIvBM_ZC_BIN 3.556205 10.0 0.35562 0.515753 -10.897465 -20.906943 -33.146916 1.311532 0.090788 309
CSGvBM_ZC_BIN 5.927567 10.0 0.592757 0.849997 -15.545957 -18.181337 -25.77121 0.811108 -0.05084 309
CSLvBM_ZC_BIN 7.23939 10.0 0.723939 1.043769 -16.21809 -33.321289 -39.09319 0.770493 0.004663 309
CSGLvBM_ZC_BIN 6.065013 10.0 0.606501 0.869332 -16.95827 -25.827802 -50.672519 0.85888 -0.001136 309
CSILvBM_ZC_BIN 7.564706 10.0 0.756471 1.119727 -18.365725 -30.8356 -37.151467 0.696419 0.053537 309
dix = dict_fxdi
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 8)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/60ea31420c6500952e28c110035b5d75bf75f8beee1f2005a3aaa5801dbd1506.png

Directional IRS strategy #

Specs and panel test #

sigs = cs_dir
ms = "CSGIL_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "DU05YXR_VT10"  # "DU02YXR_VT10"
cidx = msm.common_cids(dfx, sigs + [targ])
# cidx = list(set(cids_dm) & set(cidx))   # for DM alone

dict_dudi = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": dublack,
    "srr": None,
    "pnls": None,
}
dix = dict_dudi
cidx = dix["cidx"]
print(len(cidx))
", ".join(cidx)
26
'AUD, BRL, CAD, CHF, CLP, COP, CZK, EUR, GBP, HUF, ILS, JPY, KRW, MXN, MYR, NOK, NZD, PLN, RUB, SEK, SGD, THB, TRY, TWD, USD, ZAR'
dix = dict_dudi

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score, end of quarter",
    ylab="5-year IRS return next quarter for 10% vol target",
    title="Cyclical strength and subsequent 5-year IRS returns, 2000-2023 (Apr)",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/e1365e529a64d747213c48b8e02df68ad6891d53b99a49cfcd8e2aabb0b4503c.png
dix = dict_dudi

sig = dix["sig"]
targ = dix["targ"]
cidx = ["EUR", "USD"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score, end of quarter",
    ylab="5-year IRS return next quarter for 10% vol target",
    title="Cyclical strength and subsequent 5-year IRS returns, U.S. and euro area only, 2000-2023",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/f09b0db92367e70e4d9bb1cd756ae24b9ba0f7aaf40999e6f703da70ba37e6ee.png

Accuracy and correlation check #

dix = dict_dudi

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    sig_neg=[True] + [True] * len(rivs),
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_dudi
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGIL_ZC_NEG/last => DU05YXR_VT10 0.524 0.524 0.510 0.544 0.567 0.480 0.047 0.000 0.035 0.000 0.524
Mean years 0.517 0.513 0.503 0.553 0.567 0.458 0.033 0.348 0.023 0.369 0.511
Positive ratio 0.577 0.615 0.538 0.731 0.692 0.346 0.692 0.500 0.654 0.500 0.615
Mean cids 0.524 0.523 0.504 0.541 0.565 0.482 0.047 0.431 0.033 0.453 0.523
Positive ratio 0.808 0.846 0.423 0.962 0.962 0.308 0.846 0.538 0.808 0.538 0.846

Labor market dynamics are good predictors, labor market status is not, supporting the hypothesis that fixed-income markets are only inattentive to recent dynamics but not to the broad state of the economy.

dix = dict_dudi
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
DU05YXR_VT10 CSGIL_ZC_NEG M last 0.524 0.524 0.510 0.544 0.567 0.480 0.047 0.000 0.035 0.000 0.524
CSGI_ZC_NEG M last 0.533 0.528 0.566 0.543 0.568 0.488 0.047 0.000 0.032 0.000 0.528
CSGL_ZC_NEG M last 0.517 0.520 0.461 0.544 0.566 0.475 0.034 0.005 0.034 0.000 0.520
CSG_ZC_NEG M last 0.516 0.512 0.544 0.544 0.555 0.469 0.028 0.020 0.028 0.001 0.512
CSIL_ZC_NEG M last 0.522 0.522 0.496 0.544 0.566 0.478 0.046 0.000 0.031 0.000 0.522
CSI_ZC_NEG M last 0.527 0.523 0.550 0.543 0.563 0.482 0.039 0.001 0.028 0.001 0.523
CSL_ZC_NEG M last 0.506 0.517 0.382 0.543 0.564 0.470 0.032 0.010 0.029 0.000 0.516
dix = dict_dudi
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="Accuracy of monthly predictions of FX forward returns for 26 EM and DM currencies",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/4ee49fe9c4eab4aaf7e89732916cfe6ddb5a4f61eca6e20854477b7714e363e6.png

Naive PnL #

dix = dict_dudi

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA", "USD_DU05YXR_VT10"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=True,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=True,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_dudi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/f88e12c318db9971486d08549af58e9161d4f6dd8e7736534ecd6de5b21fe30d.png
dix = dict_dudi

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGIL_ZC_PZN":"based on negative of cyclical strength z-score",
"Long only": "receiver only portfolio across 25 currencies (risk parity)"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="5-year interest rate swap PnL across 25 markets",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/c4a63f679424b31d8d0782d668956726d32498a36ae1fc6c30b19104683a112e.png
dix = dict_dudi

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/d1e725bd5877957112c9543cdf80b9f80667caa52288e4ffafe9bed8a2bd6930.png
dix = dict_dudi

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl USD_DU05YXR_VT10 correl Traded Months
xcat
CSGIL_ZC_PZN 3.525897 10.0 0.35259 0.507844 -37.271339 -55.522607 -67.02652 1.731177 -0.034648 0.002047 309
CSGL_ZC_PZN 2.971952 10.0 0.297195 0.418339 -44.963473 -68.442356 -82.825078 1.963224 -0.016934 -0.01542 309
CSL_ZC_PZN 2.016194 10.0 0.201619 0.29166 -31.232978 -44.54905 -81.09373 2.359315 0.021078 -0.149679 309
CSIL_ZC_PZN 2.896153 10.0 0.289615 0.424846 -22.524133 -36.058254 -54.43507 1.89662 -0.024346 -0.049537 309
CSI_ZC_PZN 2.590808 10.0 0.259081 0.380374 -19.829389 -38.924861 -65.824042 1.911904 -0.045139 0.02058 309
CSGI_ZC_PZN 3.852723 10.0 0.385272 0.556724 -34.744145 -52.466814 -64.637481 1.580114 -0.054923 0.075916 309
CSG_ZC_PZN 3.590519 10.0 0.359052 0.503039 -45.713092 -71.0953 -87.498986 1.791958 -0.040107 0.099706 309
CSGIL_ZC_BIN 3.403368 10.0 0.340337 0.494006 -22.472686 -32.599258 -40.023093 1.234966 -0.024414 0.01432 309
CSGL_ZC_BIN 2.940951 10.0 0.294095 0.441333 -22.117582 -32.293696 -46.422419 1.385245 -0.012675 -0.076459 309
CSL_ZC_BIN 0.977566 10.0 0.097757 0.144059 -21.849604 -28.650985 -73.420145 3.81842 0.027905 -0.165094 309
CSIL_ZC_BIN 2.258678 10.0 0.225868 0.327605 -16.655267 -29.769599 -53.200003 1.659959 -0.000682 -0.014375 309
CSI_ZC_BIN 2.693655 10.0 0.269365 0.390384 -14.589427 -29.623148 -58.114786 1.566406 -0.042133 0.082895 309
CSGI_ZC_BIN 4.194984 10.0 0.419498 0.611325 -21.225115 -31.590582 -37.028507 1.046633 -0.040308 0.088226 309
CSG_ZC_BIN 2.804113 10.0 0.280411 0.412495 -21.837803 -31.885193 -45.015217 1.490924 -0.027202 0.057724 309
dix = dict_dudi
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 8)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/334f77ecf4cb22acc2f088374abc9c714c4d7f2338d312c4a76d0614682ff212.png

FX versus equity strategy (directional features) #

Specs and panel test #

sigs = cs_dir
ms = "CSGIL_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "FXvEQXR"
cidx = msm.common_cids(dfx, sigs + [targ])
cidx = list(set(cidx_fxeq) & set(cidx))
dict_fxeq = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_fxeq
cidx = dix["cidx"]
cidx.sort()
print(len(cidx))
", ".join(cidx)
17
'AUD, BRL, CAD, CHF, EUR, GBP, JPY, KRW, MXN, MYR, PLN, SEK, SGD, THB, TRY, TWD, ZAR'
dix = dict_fxeq

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",  # quarterly frequency allows for policy inertia
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    end = "2023-05-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score, end of quarter",
    ylab="FX forward versus equity future return next quarter (both 10% vol target)",
    title="Cyclical strength and subsequent FX versus equity returns, 2000-2023 (Apr)",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/d9b79908fb19e49bcba2c2874f4398099c5d9e52be7864e3055fc7c6afe859ab.png

Accuracy and correlation check #

dix = dict_fxeq

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_fxeq
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGIL_ZC/last => FXvEQXR 0.520 0.518 0.478 0.462 0.481 0.555 0.043 0.005 0.025 0.013 0.518
Mean years 0.519 0.513 0.482 0.464 0.476 0.551 0.042 0.443 0.013 0.407 0.510
Positive ratio 0.731 0.577 0.346 0.269 0.346 0.731 0.654 0.346 0.462 0.346 0.577
Mean cids 0.520 0.515 0.477 0.465 0.480 0.550 0.041 0.508 0.023 0.464 0.514
Positive ratio 0.750 0.688 0.438 0.125 0.312 0.812 0.688 0.562 0.812 0.500 0.688
dix = dict_fxeq
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
FXvEQXR CSGIL_ZC M last 0.520 0.518 0.478 0.462 0.481 0.555 0.043 0.005 0.025 0.013 0.518
CSGI_ZC M last 0.513 0.509 0.435 0.463 0.472 0.545 0.032 0.035 0.018 0.072 0.508
CSGL_ZC M last 0.498 0.501 0.542 0.462 0.464 0.539 0.025 0.098 0.013 0.220 0.501
CSG_ZC M last 0.502 0.498 0.453 0.463 0.461 0.536 0.007 0.653 -0.004 0.679 0.498
CSIL_ZC M last 0.516 0.515 0.489 0.462 0.478 0.553 0.061 0.000 0.035 0.001 0.515
CSI_ZC M last 0.523 0.519 0.439 0.463 0.484 0.554 0.046 0.003 0.029 0.004 0.519
CSL_ZC M last 0.506 0.514 0.595 0.462 0.474 0.555 0.044 0.005 0.031 0.003 0.514
dix = dict_fxeq
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    title="Accuracy of monthly predictions of FX forward returns for 26 EM and DM currencies",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/3c724783830588395e79c8e610cb7a058e0430ec155d9c50e2264c50fb6cc067.png

Naive PnL #

dix = dict_fxeq

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_fxeq

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/5b6ec0f29c2f71497bdaa17e74a9648ab247a7fc67578f7a550a5a97a2856787.png
dix = dict_fxeq

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGIL_ZC_PZN":"based on directional cyclical strength z-score",
"Long only": "always long FX versus equity"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="FX forward versus equity index future PnL across 17 currency areas, outright signal",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/95923efd009289bcd84772deb81ffb5aa3ab23ab9d51a365a02759eb70f9ab20.png
dix = dict_fxeq

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/a4dfed7dee367d7ed8aff04e57c4a8194ce0a4433d166b97388141e11ca7c99a.png
dix = dict_fxeq

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
CSGIL_ZC_PZN 5.169818 10.0 0.516982 0.761458 -15.691079 -16.813987 -22.72514 1.051804 0.036959 309
CSGL_ZC_PZN 3.102198 10.0 0.31022 0.460292 -16.895904 -19.089031 -30.803919 1.696437 0.036172 309
CSL_ZC_PZN 4.038529 10.0 0.403853 0.594867 -11.438465 -16.858075 -34.25559 1.060413 -0.129847 309
CSIL_ZC_PZN 5.80595 10.0 0.580595 0.841758 -18.083758 -21.26625 -26.641397 0.823578 -0.043939 309
CSI_ZC_PZN 4.809862 10.0 0.480986 0.676496 -18.718979 -20.818835 -27.382134 0.897018 0.028612 309
CSGI_ZC_PZN 4.633885 10.0 0.463388 0.668908 -15.405043 -18.474132 -26.086474 1.160973 0.110941 309
CSG_ZC_PZN 1.537301 10.0 0.15373 0.225401 -16.25558 -20.264946 -31.931124 3.268008 0.150778 309
CSGIL_ZC_BIN 4.964896 10.0 0.49649 0.718046 -12.112559 -18.33451 -22.172962 0.973571 0.021353 309
CSGL_ZC_BIN 1.265059 10.0 0.126506 0.182435 -11.436509 -23.591262 -57.887908 3.282091 -0.002645 309
CSL_ZC_BIN 2.000808 10.0 0.200081 0.28992 -13.116016 -19.311781 -50.541278 2.012927 -0.160397 309
CSIL_ZC_BIN 4.741308 10.0 0.474131 0.683969 -14.430407 -17.203007 -38.701261 0.974619 -0.009918 309
CSI_ZC_BIN 5.3801 10.0 0.53801 0.753511 -13.563163 -15.000958 -25.510717 0.801657 0.078637 309
CSGI_ZC_BIN 3.842924 10.0 0.384292 0.544485 -14.858236 -18.295963 -27.448945 1.200065 0.073542 309
CSG_ZC_BIN 1.047675 10.0 0.104768 0.148654 -17.529238 -16.73876 -38.833582 4.093185 0.121429 309
dix = dict_fxeq
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 8)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/2231c1c18232cc78505ec5f7b2d9875c44d0ddb8d5a8d40848f4ebf410da96ec.png

FX versus equity strategy (relative features) #

Specs and panel test #

sigs = cs_rel
ms = "CSGILvBM_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "FXvEQXR"
cidx = msm.common_cids(dfx, sigs + [targ])
cidx = list(set(cidx_fxeq) & set(cidx))
dict_fxeq_rf = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_fxeq_rf

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",  # quarterly frequency allows for policy inertia
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    # separator=2011,
    #     xlab="",
    #     ylab="",
    #     title="",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/5a7977582b2861fef3c6876d0e05b54328bdbac65b42c8583487f4203f710311.png

Accuracy and correlation check #

dix = dict_fxeq_rf

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_fxeq_rf
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGILvBM_ZC/last => FXvEQXR 0.519 0.513 0.414 0.462 0.477 0.548 0.076 0.000 0.038 0.000 0.512
Mean years 0.517 0.501 0.422 0.463 0.466 0.537 0.047 0.415 0.016 0.467 0.501
Positive ratio 0.577 0.500 0.346 0.231 0.346 0.692 0.692 0.423 0.500 0.346 0.500
Mean cids 0.516 0.504 0.420 0.465 0.469 0.540 0.055 0.325 0.027 0.385 0.504
Positive ratio 0.667 0.533 0.267 0.133 0.200 0.800 0.733 0.533 0.667 0.467 0.533
dix = dict_fxeq_rf
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
FXvEQXR CSGILvBM_ZC M last 0.519 0.513 0.414 0.462 0.477 0.548 0.076 0.000 0.038 0.000 0.512
CSGIvBM_ZC M last 0.506 0.501 0.445 0.462 0.463 0.540 0.058 0.000 0.025 0.021 0.501
CSGLvBM_ZC M last 0.520 0.515 0.426 0.462 0.479 0.550 0.060 0.000 0.037 0.000 0.514
CSGvBM_ZC M last 0.506 0.503 0.462 0.462 0.465 0.541 0.032 0.045 0.013 0.212 0.503
CSILvBM_ZC M last 0.519 0.515 0.435 0.462 0.478 0.551 0.076 0.000 0.044 0.000 0.514
CSIvBM_ZC M last 0.511 0.507 0.455 0.459 0.466 0.548 0.049 0.002 0.028 0.010 0.507
CSLvBM_ZC M last 0.534 0.530 0.436 0.462 0.495 0.564 0.062 0.000 0.041 0.000 0.529
dix = dict_fxeq_rf
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    title="Accuracy of monthly predictions of FX forward returns for 26 EM and DM currencies",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/294206a8d4c457553be81ec70230996afdc9c3a132bcdc89530c3f98b8d2b4b9.png

Naive PnL #

dix = dict_fxeq_rf

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_fxeq_rf

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/721717e64914fab684550a305c562a0721d928f858543dc55dd2691e8ca249dc.png
dix = dict_fxeq_rf

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGILvBM_ZC_PZN": "based on directional cyclical strength z-score",
         "Long only": "always long FX versus equity"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="FX forward versus equity index future PnL across 17 currency areas, relative signal",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/78821540412e7579e6bf715e8807a89768d1dc63cd1c12d4d0be470c7ee73c23.png
dix = dict_fxeq_rf

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/7f69751892ff013ff1d0a0196ff5eb782aa5f04ce5f5f6a4895ce6234bfe2e02.png
dix = dict_fxeq_rf

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_PZN 8.083482 10.0 0.808348 1.164849 -14.310192 -14.847543 -19.985954 0.570621 0.058455 309
CSGIvBM_ZC_PZN 6.313444 10.0 0.631344 0.911225 -11.224601 -19.525598 -34.919526 0.748892 0.08488 309
CSIvBM_ZC_PZN 5.605568 10.0 0.560557 0.80057 -15.176636 -27.053954 -54.529286 0.84033 0.079054 309
CSGvBM_ZC_PZN 3.88895 10.0 0.388895 0.559352 -11.715612 -27.006701 -41.194957 1.074876 0.0308 309
CSLvBM_ZC_PZN 6.89532 10.0 0.689532 1.008384 -14.587465 -19.737051 -29.491971 0.731201 -0.009449 309
CSGLvBM_ZC_PZN 7.152977 10.0 0.715298 1.043846 -13.647982 -22.355071 -24.999927 0.645933 0.013884 309
CSILvBM_ZC_PZN 8.082703 10.0 0.80827 1.164605 -17.03676 -12.010436 -22.93855 0.579405 0.052457 309
CSGILvBM_ZC_BIN 5.898226 10.0 0.589823 0.844275 -14.708521 -18.079324 -20.790881 0.774189 0.082996 309
CSGIvBM_ZC_BIN 2.420148 10.0 0.242015 0.339559 -15.906321 -21.639186 -50.402859 1.799904 0.065489 309
CSIvBM_ZC_BIN 3.143944 10.0 0.314394 0.433905 -16.378745 -31.188542 -54.578786 1.31773 0.093993 309
CSGvBM_ZC_BIN 5.272623 10.0 0.527262 0.768299 -13.366206 -20.489198 -23.276359 0.885324 0.022582 309
CSLvBM_ZC_BIN 7.878821 10.0 0.787882 1.144311 -14.515416 -13.876299 -20.138206 0.620371 0.048179 309
CSGLvBM_ZC_BIN 6.035709 10.0 0.603571 0.862174 -14.096442 -20.591858 -28.130951 0.789878 0.032236 309
CSILvBM_ZC_BIN 6.113362 10.0 0.611336 0.868241 -14.818696 -18.133646 -21.446588 0.67743 0.094896 309
dix = dict_fxeq_rf
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 5)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/1f440a2b04e80deb6e71dd635e3a59e1f7631156f6340d1dd6cd2fbd23f76ddd.png

FX versus IRS strategy (relative features) #

Specs and panel test #

sigs = cs_rel
ms = "CSGILvBM_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "FXvDU05XR"
cidx = msm.common_cids(dfx, sigs + [targ])
cidx = list(set(cidx_fxdu) & set(cidx))
dict_fxdu_rf = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_fxdu_rf

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",  # quarterly frequency allows for policy inertia
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score versus benchmark currency area, end of quarter",
    ylab="FX foward return versus 5-year IRS return, volatility neutral, next quarter",
    title="Relative cyclical strength and subsequent FX versus IRS returns, 2000-2023 (Apr)",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/3b6a65f759de24d4c44133abbcd827dcde0f5b351d74204f143f86c0b47da5f8.png

Accuracy and correlation check #

dix = dict_fxdu_rf

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_fxdu_rf
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGILvBM_ZC/last => FXvDU05XR 0.524 0.525 0.438 0.503 0.531 0.519 0.055 0.000 0.036 0.000 0.525
Mean years 0.524 0.523 0.441 0.498 0.517 0.529 0.078 0.397 0.045 0.374 0.522
Positive ratio 0.654 0.769 0.385 0.462 0.538 0.615 0.885 0.577 0.731 0.538 0.769
Mean cids 0.524 0.524 0.449 0.506 0.533 0.515 0.041 0.419 0.034 0.407 0.522
Positive ratio 0.696 0.696 0.348 0.696 0.739 0.565 0.696 0.478 0.696 0.478 0.696
dix = dict_fxdu_rf
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
FXvDU05XR CSGILvBM_ZC M last 0.524 0.525 0.438 0.503 0.531 0.519 0.055 0.000 0.036 0.000 0.525
CSGIvBM_ZC M last 0.511 0.512 0.460 0.504 0.517 0.507 0.044 0.001 0.025 0.005 0.512
CSGLvBM_ZC M last 0.516 0.517 0.454 0.503 0.521 0.512 0.039 0.004 0.028 0.001 0.517
CSGvBM_ZC M last 0.505 0.506 0.470 0.504 0.510 0.501 0.019 0.148 0.008 0.347 0.506
CSILvBM_ZC M last 0.519 0.519 0.456 0.502 0.523 0.516 0.058 0.000 0.039 0.000 0.519
CSIvBM_ZC M last 0.510 0.510 0.474 0.502 0.513 0.507 0.039 0.004 0.025 0.006 0.510
CSLvBM_ZC M last 0.524 0.525 0.440 0.501 0.529 0.521 0.044 0.001 0.033 0.000 0.524
dix = dict_fxdu_rf
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="Accuracy of monthly predictions of FX forward returns for 26 EM and DM currencies",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/0fcf54abf49c864efb5364e10de8b7f12355d82a3a301abf6b3cfc7633f8c201.png

Naive PnL #

dix = dict_fxdu_rf

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_fxdu_rf

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/fa2217468f6dd5270203b1dae6cd63183b9e79cef3247f374e78429ade658953.png
dix = dict_fxdu_rf

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGILvBM_ZC_PZN": "based on cyclical strength z-score",
         "Long only": "always long FX forward and paying 5-year IRS yields"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="FX versus duration PnL across 23 markets",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/e97c9958ddc14f2c709ae7dbeb1397a3c693753efd89f81f81881b9952e20bf6.png
dix = dict_fxdu_rf

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/74aa60ccd31a147c9bad615af6c996e0ecf6b591e98e8dcbffec50d47d6dafe3.png
dix = dict_fxdu_rf

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    end = "2023-05-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_PZN 4.912488 10.421552 0.471378 0.700675 -19.37651 -52.972411 -61.623264 1.030197 0.064582 280
CSGIvBM_ZC_PZN 3.559327 10.405497 0.342062 0.506889 -18.521322 -47.004713 -55.912441 1.135823 0.014265 280
CSIvBM_ZC_PZN 3.082582 10.355905 0.297664 0.43523 -16.853245 -38.759421 -44.384778 1.242409 0.040871 280
CSGvBM_ZC_PZN 1.902525 10.292212 0.184851 0.270007 -15.130901 -32.010849 -38.212156 2.006272 -0.021906 280
CSLvBM_ZC_PZN 3.89658 10.433973 0.373451 0.542661 -21.869749 -51.921191 -58.445681 1.283922 0.097701 280
CSGLvBM_ZC_PZN 3.911113 10.364509 0.377356 0.562606 -19.050799 -41.932182 -50.576036 1.420563 0.056851 280
CSILvBM_ZC_PZN 4.815963 10.421377 0.462123 0.675871 -19.617924 -48.10065 -55.724592 1.032373 0.091311 280
CSGILvBM_ZC_BIN 4.672824 10.353187 0.451342 0.676947 -11.76589 -31.240552 -42.400618 0.806604 0.029461 280
CSGIvBM_ZC_BIN 1.409272 10.34136 0.136275 0.191497 -18.769197 -27.594222 -40.420528 2.073046 -0.0094 280
CSIvBM_ZC_BIN 1.313849 10.328963 0.1272 0.176967 -17.319938 -20.353542 -49.880324 2.145796 0.01782 280
CSGvBM_ZC_BIN 1.431431 10.310192 0.138837 0.194328 -18.428461 -18.897902 -31.098763 2.262581 -0.024944 280
CSLvBM_ZC_BIN 4.254947 10.347364 0.411211 0.616453 -13.825385 -32.656622 -36.028791 1.044255 0.052922 280
CSGLvBM_ZC_BIN 2.610978 10.324464 0.252892 0.37358 -16.803616 -32.059031 -35.365412 1.637016 0.034051 280
CSILvBM_ZC_BIN 3.759526 10.343607 0.363464 0.538808 -14.328251 -32.205476 -39.078542 0.957085 0.05527 280
dix = dict_fxdu_rf
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 6)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/611105d2366f88378da42124c46b1d777270b9804b69609387724cce56491918.png

IRS curve flattening strategy #

Specs and panel test #

sigs = cs_dir
ms = "CSGIL_ZC"  # main signal
oths = list(set(sigs) - set([ms]))  # other signals

targ = "DU05v02XR"
cidx = msm.common_cids(dfx, sigs + [targ])
cidx = list(set(cidx_du52) & set(cidx))

dict_du52 = {
    "sig": ms,
    "rivs": oths,
    "targ": targ,
    "cidx": cidx,
    "black": dublack,
    "srr": None,
    "pnls": None,
}
dix = dict_du52

sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
    xcat_trims=[None, None],
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Cyclical strength composite score, end of quarter",
    ylab="IRS curve 2s-5s flattening return next quarter",
    title="Cyclical strength and subsequent IRS flattening returns, 2000-2023 (Apr)",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/aa126d0fb9a14db688aceb64dc4fe87cdd91c0458db6cc164d7a4c02080e498c.png

Accuracy and correlation check #

dix = dict_du52

sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2000-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_du52
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: CSGIL_ZC/last => DU05v02XR 0.533 0.534 0.490 0.516 0.550 0.517 0.085 0.000 0.053 0.000 0.534
Mean years 0.532 0.515 0.497 0.515 0.527 0.503 0.021 0.412 0.026 0.360 0.515
Positive ratio 0.731 0.577 0.462 0.577 0.615 0.462 0.615 0.346 0.654 0.423 0.577
Mean cids 0.533 0.536 0.496 0.516 0.549 0.523 0.098 0.276 0.060 0.255 0.535
Positive ratio 0.846 0.885 0.538 0.538 0.923 0.654 0.846 0.731 0.846 0.731 0.885
dix = dict_du52
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
DU05v02XR CSGIL_ZC M last 0.533 0.534 0.490 0.516 0.550 0.517 0.085 0.00 0.053 0.000 0.534
CSGI_ZC M last 0.533 0.536 0.434 0.516 0.556 0.516 0.083 0.00 0.057 0.000 0.535
CSGL_ZC M last 0.534 0.533 0.539 0.516 0.546 0.519 0.091 0.00 0.057 0.000 0.533
CSG_ZC M last 0.529 0.530 0.456 0.516 0.548 0.512 0.092 0.00 0.059 0.000 0.530
CSIL_ZC M last 0.522 0.522 0.504 0.517 0.538 0.505 0.044 0.00 0.028 0.001 0.522
CSI_ZC M last 0.515 0.517 0.450 0.517 0.536 0.498 0.023 0.06 0.018 0.031 0.517
CSL_ZC M last 0.530 0.527 0.618 0.516 0.537 0.517 0.052 0.00 0.033 0.000 0.526
dix = dict_du52
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="",
    size=(14, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/919d2985c94e78b7a2121edcb72e8fbf1fb24de32212afb31caf85b06895c8bb.png

Naive PnL #

dix = dict_du52

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2000-01-01",
    blacklist=blax,
    bms=["USD_EQXR_NSA", "USD_DU05YXR_VT10"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="zn_score_pan",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="binary",
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_BIN",
    )

naive_pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_du52

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + x for x in ["_PZN", "_BIN"]] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/2ba3d94f40bf3ce96076ca78df31ee451eb91077d00728c89f7cc378512e99b0.png
dix = dict_du52

sigx = dix["sig"]
naive_pnl = dix["pnls"]
pnls = [sigx + "_PZN"] + ["Long only"]

dict_labels={"CSGIL_ZC_PZN": "based on negative of cyclical strength z-score",
             "Long only": "always long 5-year versus 2-year, volatility neutral"}


naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="IRS curve flattening PnL across 25 markets",
    xcat_labels=dict_labels,
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/a418612cbe50fe33e778b52a9ec90fa2a5625133b65ec0f8b3b5b28275c22fa9.png
dix = dict_du52

sigx = dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title=None,
    xcat_labels=None,
    figsize=(16, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/1b85cc84ca68433b05daedcdf1d1b2bc9291e0360678c9eb2f34c5334d85de16.png
dix = dict_du52

sigx = [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + type for sig in sigx for type in ["_PZN", "_BIN"]]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval.transpose())
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl USD_DU05YXR_VT10 correl Traded Months
xcat
CSGIL_ZC_PZN 8.47011 10.0 0.847011 1.401181 -12.787125 -15.998431 -29.330038 1.025255 0.021106 -0.079211 309
CSGL_ZC_PZN 9.601829 10.0 0.960183 1.611301 -15.473662 -14.443884 -26.868499 0.880683 0.011736 -0.034447 309
CSL_ZC_PZN 6.964798 10.0 0.69648 1.081291 -19.00224 -19.361389 -26.441119 1.009315 0.010803 0.025268 309
CSIL_ZC_PZN 5.398559 10.0 0.539856 0.825882 -13.970309 -32.026307 -56.394453 1.448674 0.025254 -0.075813 309
CSI_ZC_PZN 2.700855 10.0 0.270085 0.392692 -14.09786 -33.713183 -76.72231 2.607924 0.027649 -0.117926 309
CSGI_ZC_PZN 8.015377 10.0 0.801538 1.31322 -14.814416 -14.785212 -28.913103 1.08707 0.022146 -0.113287 309
CSG_ZC_PZN 9.989959 10.0 0.998996 1.700657 -18.942765 -15.453181 -28.848625 0.888541 0.00829 -0.069479 309
CSGIL_ZC_BIN 7.364657 10.0 0.736466 1.107838 -13.394501 -22.629973 -38.189085 0.92758 0.024224 -0.078717 309
CSGL_ZC_BIN 8.01641 10.0 0.801641 1.168201 -18.901382 -19.843273 -27.826144 0.806404 0.014926 0.020684 309
CSL_ZC_BIN 6.481319 10.0 0.648132 0.938585 -16.021632 -20.99962 -42.222876 0.902598 0.002064 0.068714 309
CSIL_ZC_BIN 5.035397 10.0 0.50354 0.742288 -12.941132 -30.251291 -75.11545 1.271011 0.016545 -0.091614 309
CSI_ZC_BIN 2.812126 10.0 0.281213 0.406148 -12.004438 -30.080093 -77.251124 2.219592 0.033422 -0.152885 309
CSGI_ZC_BIN 7.768498 10.0 0.77685 1.172176 -13.488983 -20.128533 -40.812499 0.920266 0.019445 -0.127548 309
CSG_ZC_BIN 8.018084 10.0 0.801808 1.203845 -13.89593 -14.721555 -32.660932 0.862725 -0.005688 -0.037057 309
dix = dict_du52
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="m", start="2000-01-01", figsize=(16, 6)
)
https://macrosynergy.com/notebooks.build/trading-factors/macroeconomic-cycles-and-asset-class-returns/_images/f637a18cd665a6d8bd02672a43d5ff9b49baadc01c2ca195efcc7d5b28e29595.png