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 numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns


import macrosynergy.management as msm
import macrosynergy.panel as msp
import macrosynergy.signal as mss
import macrosynergy.pnl as msn
from macrosynergy.download import JPMaQSDownload

from datetime import timedelta, date, datetime
from itertools import combinations
import warnings
import os

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,
        end_date=end_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:  2023-06-15 08:54:01
Connection successful!
Number of expressions requested: 930
Requesting data: 100%|██████████| 47/47 [00:14<00:00,  3.30it/s]
Downloading data: 100%|██████████| 47/47 [00:22<00:00,  2.06it/s]
Time taken to download data: 	38.47 seconds.
Time taken to convert to dataframe: 	14.78 seconds.
Average upload size: 	0.20 KB
Average download size: 	131732.08 KB
Average time taken: 	12.69 seconds
Longest time taken: 	17.68 seconds
Average transfer rate : 	83065.35 Kbps
display(df["xcat"].unique())
display(df["cid"].unique())
df["ticker"] = df["cid"] + "_" + df["xcat"]
df.head(3)
array(['CPIC_SA_P1M1ML12', 'CPIC_SJA_P6M6ML6AR', 'CPIH_SA_P1M1ML12',
       'CPIH_SJA_P6M6ML6AR', 'EMPL_NSA_P1M1ML12_3MMA', 'FXTARGETED_NSA',
       'FXUNTRADABLE_NSA', 'FXXRHvGDRB_NSA', 'FXXR_NSA', 'FXXR_VT10',
       'INFTEFF_NSA', 'INTRGDPv5Y_NSA_P1M1ML12_3MMA',
       'PCREDITBN_SJA_P1M1ML12', 'RGDP_SA_P1Q1QL4_20QMM',
       'UNEMPLRATE_NSA_3MMA_D1M1ML12', 'UNEMPLRATE_SA_3MMA',
       'UNEMPLRATE_SA_D3M3ML3', 'WFORCE_NSA_P1Y1YL1_5YMM', 'DU02YXR_NSA',
       'DU02YXR_VT10', 'DU05YXR_VT10', 'EQXR_NSA', 'EQXR_VT10',
       'EMPL_NSA_P1Q1QL4', 'UNEMPLRATE_SA_3MMAv10YMM',
       'UNEMPLRATE_NSA_D1Q1QL4', 'WFORCE_NSA_P1Q1QL4_20QMM',
       'UNEMPLRATE_SA_D1Q1QL1', 'GB10YXR_NSA'], 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 2000-01-03 AUD CPIC_SA_P1M1ML12 1.244168 AUD_CPIC_SA_P1M1ML12
1 2000-01-03 AUD CPIC_SJA_P6M6ML6AR 1.428580 AUD_CPIC_SJA_P6M6ML6AR
2 2000-01-03 AUD CPIH_SA_P1M1ML12 1.647446 AUD_CPIH_SA_P1M1ML12
scols = ["cid", "xcat", "real_date", "value"]  # required columns
dfx = df[scols].copy()
dfx.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4003074 entries, 0 to 4003073
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: 122.2+ 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-05-02 00:00:00'), Timestamp('2016-06-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('2023-05-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('2023-05-01 00:00:00')),
 'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2023-05-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('2023-05-01 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)
_images/Macroeconomic cycles and asset class returns_18_0.png _images/Macroeconomic cycles and asset class returns_18_1.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)
_images/Macroeconomic cycles and asset class returns_23_0.png _images/Macroeconomic cycles and asset class returns_23_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_29_0.png _images/Macroeconomic cycles and asset class returns_29_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_30_0.png _images/Macroeconomic cycles and asset class returns_30_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_33_0.png _images/Macroeconomic cycles and asset class returns_33_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_37_0.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_43_0.png _images/Macroeconomic cycles and asset class returns_43_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_45_0.png _images/Macroeconomic cycles and asset class returns_45_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_50_0.png _images/Macroeconomic cycles and asset class returns_50_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_55_0.png _images/Macroeconomic cycles and asset class returns_55_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_59_0.png _images/Macroeconomic cycles and asset class returns_59_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_62_0.png _images/Macroeconomic cycles and asset class returns_62_1.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,
    aspect=1.5,
    size=(12, 12),
    all_xticks=True,
    title="Excess GDP growth z-scores",
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_66_0.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,
    aspect=1.5,
    size=(12, 12),
    all_xticks=True,
    title="Labor market tightness composite z-scores",
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_67_0.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,
    aspect=1.5,
    size=(12, 12),
    all_xticks=True,
    title="Excess CPI inflation z-scores",
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_68_0.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,
    aspect=1.5,
    size=(12, 12),
    all_xticks=True,
    title="Composite cyclical strength scores, outright and versus benchmark currency area",
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_69_0.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_72_0.png _images/Macroeconomic cycles and asset class returns_72_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_74_0.png _images/Macroeconomic cycles and asset class returns_74_1.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_76_0.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_79_0.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_82_0.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,
    aspect=1.75,
    size=(12, 12),
    all_xticks=True,
    title_adj=0.95,
    label_adj=0.05,
)
_images/Macroeconomic cycles and asset class returns_85_0.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",
)
_images/Macroeconomic cycles and asset class returns_90_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=True,
    ret=targ,
    freq="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
Panel 0.526 0.523 0.515 0.593 0.615 0.431 0.110 0.000 0.060 0.000
Mean years 0.520 0.509 0.499 0.592 0.596 0.421 0.047 0.415 0.025 0.487
Positive ratio 0.583 0.583 0.583 0.667 0.708 0.292 0.542 0.375 0.542 0.375
Mean cids 0.526 0.522 0.512 0.589 0.610 0.433 0.112 0.208 0.056 0.299
Positive ratio 0.706 0.588 0.588 0.941 0.941 0.000 0.941 0.882 0.824 0.647
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
CSGIL_ZC_NEG 0.526 0.523 0.515 0.593 0.615 0.431 0.110 0.0 0.060 0.000
CSGI_ZC_NEG 0.536 0.526 0.555 0.593 0.616 0.436 0.099 0.0 0.056 0.000
CSGL_ZC_NEG 0.500 0.507 0.462 0.593 0.600 0.413 0.084 0.0 0.039 0.000
CSG_ZC_NEG 0.519 0.512 0.535 0.593 0.604 0.420 0.063 0.0 0.022 0.038
CSIL_ZC_NEG 0.528 0.528 0.498 0.593 0.621 0.436 0.114 0.0 0.067 0.000
CSI_ZC_NEG 0.538 0.528 0.553 0.593 0.619 0.438 0.092 0.0 0.054 0.000
CSL_ZC_NEG 0.498 0.516 0.406 0.594 0.613 0.419 0.082 0.0 0.046 0.000
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),
)
_images/Macroeconomic cycles and asset class returns_95_0.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),
)
_images/Macroeconomic cycles and asset class returns_98_0.png
dix = dict_eqdi

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="Equity index future PnL across 18 markets",
    xcat_labels=[
        "based on negative of cyclical strength z-score",
        "long only portfolio across 18 currencies (risk parity)",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_99_0.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),
)
_images/Macroeconomic cycles and asset class returns_100_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl Traded Months
xcat
CSGIL_ZC_BIN 6.209237 10.0 0.620924 0.91677 -12.431751 -15.669608 -0.059427 280
CSGIL_ZC_PZN 7.585781 10.0 0.758578 1.161973 -16.094502 -16.324447 0.007458 280
CSGI_ZC_BIN 6.225956 10.0 0.622596 0.897637 -15.530303 -18.792508 0.025745 280
CSGI_ZC_PZN 7.335541 10.0 0.733554 1.106905 -14.659262 -16.112356 0.090681 280
CSGL_ZC_BIN 1.172735 10.0 0.117273 0.172006 -12.532228 -20.012622 0.017558 280
CSGL_ZC_PZN 5.66139 10.0 0.566139 0.87363 -16.064694 -17.86919 0.090345 280
CSG_ZC_BIN 2.173434 10.0 0.217343 0.311485 -22.70855 -22.628741 0.197863 280
CSG_ZC_PZN 4.683366 10.0 0.468337 0.717618 -14.037185 -25.245085 0.244934 280
CSIL_ZC_BIN 6.592618 10.0 0.659262 0.968538 -13.65691 -16.605895 -0.142414 280
CSIL_ZC_PZN 7.561621 10.0 0.756162 1.126925 -19.910431 -23.810352 -0.164604 280
CSI_ZC_BIN 7.361555 10.0 0.736156 1.05005 -22.217076 -16.880745 -0.023381 280
CSI_ZC_PZN 6.641288 10.0 0.664129 0.965157 -19.895999 -25.145329 -0.113013 280
CSL_ZC_BIN 3.27155 10.0 0.327155 0.494351 -12.880212 -19.266338 -0.223866 280
CSL_ZC_PZN 5.070467 10.0 0.507047 0.761298 -16.780684 -14.729327 -0.158112 280
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)
)
_images/Macroeconomic cycles and asset class returns_102_0.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",
    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",
)
_images/Macroeconomic cycles and asset class returns_106_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="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
Panel 0.527 0.532 0.449 0.546 0.581 0.483 0.078 0.000 0.054 0.000
Mean years 0.526 0.523 0.447 0.546 0.570 0.476 0.063 0.317 0.040 0.292
Positive ratio 0.625 0.792 0.417 0.708 0.792 0.375 0.750 0.667 0.708 0.625
Mean cids 0.527 0.528 0.452 0.547 0.579 0.477 0.070 0.342 0.046 0.327
Positive ratio 0.741 0.815 0.259 0.889 0.852 0.370 0.815 0.667 0.815 0.593
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
CSGILvBM_ZC 0.527 0.532 0.449 0.546 0.581 0.483 0.078 0.00 0.054 0.000
CSGIvBM_ZC 0.515 0.518 0.467 0.545 0.564 0.472 0.055 0.00 0.035 0.000
CSGLvBM_ZC 0.521 0.522 0.485 0.546 0.568 0.476 0.060 0.00 0.046 0.000
CSGvBM_ZC 0.507 0.508 0.482 0.541 0.549 0.467 0.022 0.07 0.015 0.065
CSILvBM_ZC 0.531 0.535 0.465 0.545 0.582 0.487 0.088 0.00 0.061 0.000
CSIvBM_ZC 0.517 0.520 0.459 0.542 0.564 0.476 0.060 0.00 0.038 0.000
CSLvBM_ZC 0.531 0.533 0.478 0.544 0.578 0.487 0.077 0.00 0.058 0.000
dix = dict_fxdi
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="",
    size=(14, 6),
)
_images/Macroeconomic cycles and asset class returns_111_0.png

Naive PnL#

dfx
cid xcat real_date value
0 AUD CPIC_SA_P1M1ML12 2000-01-03 1.244168
1 AUD CPIC_SA_P1M1ML12 2000-01-04 1.244168
2 AUD CPIC_SA_P1M1ML12 2000-01-05 1.244168
3 AUD CPIC_SA_P1M1ML12 2000-01-06 1.244168
4 AUD CPIC_SA_P1M1ML12 2000-01-07 1.244168
... ... ... ... ...
10450475 ZAR XEMPL_NSA_P1M1ML12_3MMA_ZNP 2023-04-25 3.000000
10450476 ZAR XEMPL_NSA_P1M1ML12_3MMA_ZNP 2023-04-26 3.000000
10450477 ZAR XEMPL_NSA_P1M1ML12_3MMA_ZNP 2023-04-27 3.000000
10450478 ZAR XEMPL_NSA_P1M1ML12_3MMA_ZNP 2023-04-28 3.000000
10450479 ZAR XEMPL_NSA_P1M1ML12_3MMA_ZNP 2023-05-01 3.000000

10450480 rows × 4 columns

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),
)
_images/Macroeconomic cycles and asset class returns_115_0.png
dix = dict_fxdi

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

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=[
        "based on relative cyclical strength z-score",
        "long only portfolio in all 27 smaller currencies (versus USD and EUR, risk parity)",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_116_0.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),
)
_images/Macroeconomic cycles and asset class returns_117_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_BIN 7.854542 10.0 0.785454 1.180248 -11.273397 -22.143409 0.005345 280
CSGILvBM_ZC_PZN 9.597865 10.0 0.959787 1.436437 -12.300822 -27.115989 0.062458 280
CSGIvBM_ZC_BIN 4.132156 10.0 0.413216 0.606083 -12.576245 -18.233128 0.037903 280
CSGIvBM_ZC_PZN 7.813255 10.0 0.781325 1.167744 -9.972963 -19.87863 0.053369 280
CSGLvBM_ZC_BIN 5.818748 10.0 0.581875 0.837242 -17.569606 -24.982273 0.000619 280
CSGLvBM_ZC_PZN 8.089174 10.0 0.808917 1.193029 -12.852974 -28.141812 0.017982 280
CSGvBM_ZC_BIN 3.187497 10.0 0.31875 0.449135 -15.573575 -19.526821 -0.039887 280
CSGvBM_ZC_PZN 4.182742 10.0 0.418274 0.603775 -13.551429 -20.716411 -0.0389 280
CSILvBM_ZC_BIN 8.503413 10.0 0.850341 1.274676 -14.189752 -27.121965 0.047086 280
CSILvBM_ZC_PZN 9.345831 10.0 0.934583 1.398067 -15.83859 -30.702561 0.095541 280
CSIvBM_ZC_BIN 4.227642 10.0 0.422764 0.62317 -10.516554 -19.921258 0.070549 280
CSIvBM_ZC_PZN 7.186504 10.0 0.71865 1.095897 -12.173519 -23.223538 0.108469 280
CSLvBM_ZC_BIN 8.091304 10.0 0.80913 1.202256 -16.3747 -31.691633 -0.000977 280
CSLvBM_ZC_PZN 8.126893 10.0 0.812689 1.182102 -20.380927 -37.593219 0.058713 280
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)
)
_images/Macroeconomic cycles and asset class returns_119_0.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)
25
'AUD, 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",
)
_images/Macroeconomic cycles and asset class returns_124_0.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",
)
_images/Macroeconomic cycles and asset class returns_125_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=True,
    ret=targ,
    freq="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
Panel 0.523 0.522 0.505 0.552 0.574 0.471 0.042 0.001 0.029 0.001
Mean years 0.512 0.511 0.489 0.564 0.579 0.443 0.003 0.434 0.007 0.425
Positive ratio 0.542 0.625 0.500 0.792 0.792 0.250 0.500 0.333 0.500 0.292
Mean cids 0.523 0.522 0.503 0.550 0.574 0.470 0.043 0.453 0.028 0.502
Positive ratio 0.840 0.680 0.480 0.960 1.000 0.120 0.760 0.600 0.760 0.560

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
CSGIL_ZC_NEG 0.523 0.522 0.505 0.552 0.574 0.471 0.042 0.001 0.029 0.001
CSGI_ZC_NEG 0.523 0.517 0.560 0.551 0.566 0.467 0.042 0.001 0.025 0.003
CSGL_ZC_NEG 0.512 0.515 0.468 0.552 0.568 0.463 0.034 0.009 0.031 0.000
CSG_ZC_NEG 0.522 0.517 0.542 0.552 0.568 0.467 0.030 0.019 0.026 0.003
CSIL_ZC_NEG 0.516 0.516 0.491 0.552 0.569 0.464 0.039 0.003 0.027 0.002
CSI_ZC_NEG 0.523 0.518 0.549 0.551 0.567 0.469 0.031 0.020 0.024 0.006
CSL_ZC_NEG 0.498 0.510 0.381 0.551 0.564 0.457 0.029 0.025 0.027 0.002
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),
)
_images/Macroeconomic cycles and asset class returns_131_0.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),
)
_images/Macroeconomic cycles and asset class returns_134_0.png
dix = dict_dudi

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

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=[
        "based on negative of cyclical strength z-score",
        "receiver only portfolio across 25 currencies (risk parity)",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_135_0.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),
)
_images/Macroeconomic cycles and asset class returns_136_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl USD_DU05YXR_VT10 correl Traded Months
xcat
CSGIL_ZC_BIN 4.405342 10.0 0.440534 0.605293 -33.909119 -48.428222 -0.01592 -0.021337 280
CSGIL_ZC_PZN 3.734646 10.0 0.373465 0.51338 -45.867272 -66.912175 -0.035862 -0.014989 280
CSGI_ZC_BIN 4.911954 10.0 0.491195 0.682531 -31.560109 -46.63644 -0.053309 0.097057 280
CSGI_ZC_PZN 4.299792 10.0 0.429979 0.598321 -41.823099 -62.245512 -0.060966 0.070491 280
CSGL_ZC_BIN 3.075311 10.0 0.307531 0.429297 -35.141034 -51.697788 -0.003701 -0.142086 280
CSGL_ZC_PZN 3.32427 10.0 0.332427 0.452546 -51.890771 -78.263209 -0.012379 -0.041399 280
CSG_ZC_BIN 5.790887 10.0 0.579089 0.821286 -34.566861 -50.853094 -0.067622 0.063298 280
CSG_ZC_PZN 4.215108 10.0 0.421511 0.582604 -49.481644 -76.383723 -0.039333 0.077436 280
CSIL_ZC_BIN 1.98139 10.0 0.198139 0.272578 -26.405647 -45.439707 -0.000145 -0.037018 280
CSIL_ZC_PZN 3.097892 10.0 0.309789 0.427729 -33.449798 -48.986926 -0.029443 -0.06827 280
CSI_ZC_BIN 4.044225 10.0 0.404423 0.570853 -18.866363 -46.794599 -0.056608 0.096652 280
CSI_ZC_PZN 3.019308 10.0 0.301931 0.42228 -20.471128 -56.609488 -0.063606 0.029531 280
CSL_ZC_BIN -0.654195 10.0 -0.065419 -0.089368 -34.843895 -45.661604 0.056245 -0.26432 280
CSL_ZC_PZN 1.919463 10.0 0.191946 0.262454 -42.448418 -59.333586 0.032876 -0.204209 280
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)
)
_images/Macroeconomic cycles and asset class returns_138_0.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",
    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",
)
_images/Macroeconomic cycles and asset class returns_143_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="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
Panel 0.522 0.520 0.477 0.458 0.480 0.561 0.047 0.004 0.029 0.008
Mean years 0.520 0.521 0.492 0.458 0.467 0.574 0.051 0.398 0.022 0.370
Positive ratio 0.708 0.500 0.417 0.250 0.375 0.792 0.667 0.417 0.583 0.417
Mean cids 0.523 0.516 0.481 0.461 0.477 0.554 0.043 0.504 0.022 0.500
Positive ratio 0.812 0.750 0.375 0.125 0.188 0.750 0.812 0.562 0.688 0.312
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
CSGIL_ZC 0.522 0.520 0.477 0.458 0.480 0.561 0.047 0.004 0.029 0.008
CSGI_ZC 0.512 0.508 0.444 0.458 0.467 0.549 0.035 0.031 0.021 0.058
CSGL_ZC 0.497 0.500 0.532 0.458 0.458 0.541 0.028 0.086 0.015 0.173
CSG_ZC 0.502 0.499 0.462 0.458 0.457 0.541 0.009 0.600 -0.003 0.752
CSIL_ZC 0.520 0.519 0.494 0.458 0.478 0.561 0.066 0.000 0.041 0.000
CSI_ZC 0.526 0.522 0.446 0.459 0.483 0.561 0.050 0.002 0.034 0.002
CSL_ZC 0.512 0.520 0.592 0.458 0.474 0.566 0.047 0.004 0.033 0.002
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),
)
_images/Macroeconomic cycles and asset class returns_148_0.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),
)
_images/Macroeconomic cycles and asset class returns_151_0.png
dix = dict_fxeq

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

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=[
        "based on directional cyclical strength z-score",
        "always long FX versus equity",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_152_0.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),
)
_images/Macroeconomic cycles and asset class returns_153_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl Traded Months
xcat
CSGIL_ZC_BIN 5.473413 10.0 0.547341 0.799478 -14.342208 -18.237097 -0.009847 280
CSGIL_ZC_PZN 5.637239 10.0 0.563724 0.834991 -14.326246 -17.218382 0.02364 280
CSGI_ZC_BIN 4.12793 10.0 0.412793 0.589656 -14.693957 -18.656172 0.050219 280
CSGI_ZC_PZN 5.163066 10.0 0.516307 0.748861 -13.953859 -19.567337 0.095109 280
CSGL_ZC_BIN 1.274374 10.0 0.127437 0.184417 -12.499694 -26.046865 -0.001857 280
CSGL_ZC_PZN 3.603917 10.0 0.360392 0.533314 -15.147175 -18.832735 0.032766 280
CSG_ZC_BIN 1.459622 10.0 0.145962 0.207716 -17.603699 -16.244174 0.112153 280
CSG_ZC_PZN 2.304889 10.0 0.230489 0.337161 -14.973557 -22.286768 0.144702 280
CSIL_ZC_BIN 5.386723 10.0 0.538672 0.792168 -14.25229 -17.203636 -0.042841 280
CSIL_ZC_PZN 6.189663 10.0 0.618966 0.909842 -17.868321 -21.083554 -0.060368 280
CSI_ZC_BIN 5.89931 10.0 0.589931 0.839733 -13.60995 -15.009403 0.055969 280
CSI_ZC_PZN 5.20329 10.0 0.520329 0.746572 -18.547016 -21.267046 0.01265 280
CSL_ZC_BIN 2.666485 10.0 0.266649 0.388517 -9.974216 -18.753122 -0.17352 280
CSL_ZC_PZN 3.998199 10.0 0.39982 0.587683 -12.334004 -17.003443 -0.131564 280
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)
)
_images/Macroeconomic cycles and asset class returns_155_0.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",
)
_images/Macroeconomic cycles and asset class returns_159_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="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
Panel 0.527 0.521 0.409 0.459 0.483 0.558 0.082 0.000 0.046 0.000
Mean years 0.527 0.514 0.419 0.458 0.473 0.555 0.053 0.420 0.023 0.463
Positive ratio 0.625 0.625 0.375 0.250 0.375 0.792 0.708 0.417 0.542 0.375
Mean cids 0.527 0.513 0.418 0.462 0.475 0.550 0.057 0.351 0.030 0.438
Positive ratio 0.733 0.667 0.200 0.133 0.200 0.867 0.733 0.600 0.667 0.467
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
CSGILvBM_ZC 0.527 0.521 0.409 0.459 0.483 0.558 0.082 0.000 0.046 0.000
CSGIvBM_ZC 0.511 0.507 0.450 0.458 0.466 0.548 0.063 0.000 0.031 0.006
CSGLvBM_ZC 0.521 0.516 0.432 0.459 0.477 0.555 0.062 0.000 0.042 0.000
CSGvBM_ZC 0.497 0.494 0.458 0.458 0.452 0.536 0.031 0.068 0.013 0.255
CSILvBM_ZC 0.525 0.520 0.429 0.459 0.481 0.559 0.087 0.000 0.052 0.000
CSIvBM_ZC 0.518 0.513 0.446 0.456 0.470 0.556 0.059 0.000 0.036 0.001
CSLvBM_ZC 0.533 0.528 0.441 0.458 0.490 0.567 0.066 0.000 0.043 0.000
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),
)
_images/Macroeconomic cycles and asset class returns_164_0.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),
)
_images/Macroeconomic cycles and asset class returns_167_0.png
dix = dict_fxeq_rf

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

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=[
        "based on directional cyclical strength z-score",
        "always long FX versus equity",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_168_0.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),
)
_images/Macroeconomic cycles and asset class returns_169_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_BIN 7.995227 10.0 0.799523 1.166073 -13.974032 -17.821373 0.057314 280
CSGILvBM_ZC_PZN 8.978731 10.0 0.897873 1.310166 -12.03718 -14.512015 0.039004 280
CSGIvBM_ZC_BIN 3.585168 10.0 0.358517 0.513629 -10.751986 -15.288601 0.03734 280
CSGIvBM_ZC_PZN 7.311336 10.0 0.731134 1.073931 -9.674447 -19.271167 0.066916 280
CSGLvBM_ZC_BIN 6.923901 10.0 0.69239 0.99924 -13.414423 -17.397104 -0.00235 280
CSGLvBM_ZC_PZN 7.627091 10.0 0.762709 1.113063 -11.896544 -21.675721 0.000997 280
CSGvBM_ZC_BIN 3.817913 10.0 0.381791 0.549283 -13.29784 -16.598154 0.018381 280
CSGvBM_ZC_PZN 4.336064 10.0 0.433606 0.625294 -12.582797 -26.916802 0.021347 280
CSILvBM_ZC_BIN 6.948016 10.0 0.694802 0.995581 -14.093154 -15.437461 0.07076 280
CSILvBM_ZC_PZN 8.866583 10.0 0.886658 1.295357 -14.195039 -12.558834 0.029902 280
CSIvBM_ZC_BIN 4.461779 10.0 0.446178 0.628016 -14.77922 -30.054623 0.081411 280
CSIvBM_ZC_PZN 6.606129 10.0 0.660613 0.970082 -12.368606 -26.436517 0.063339 280
CSLvBM_ZC_BIN 7.907721 10.0 0.790772 1.14666 -14.001979 -13.087739 0.034266 280
CSLvBM_ZC_PZN 7.034954 10.0 0.703495 1.023535 -14.300113 -18.606696 -0.02573 280
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)
)
_images/Macroeconomic cycles and asset class returns_171_0.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",
)
_images/Macroeconomic cycles and asset class returns_175_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="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
Panel 0.523 0.523 0.436 0.499 0.526 0.521 0.051 0.000 0.034 0.000
Mean years 0.525 0.524 0.438 0.489 0.509 0.538 0.062 0.430 0.040 0.387
Positive ratio 0.583 0.625 0.458 0.417 0.417 0.667 0.708 0.458 0.708 0.458
Mean cids 0.523 0.523 0.441 0.501 0.527 0.519 0.033 0.386 0.029 0.420
Positive ratio 0.818 0.818 0.227 0.591 0.682 0.636 0.545 0.455 0.591 0.455
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
CSGILvBM_ZC 0.523 0.523 0.436 0.499 0.526 0.521 0.051 0.000 0.034 0.000
CSGIvBM_ZC 0.508 0.508 0.466 0.501 0.510 0.506 0.037 0.009 0.022 0.025
CSGLvBM_ZC 0.517 0.517 0.465 0.499 0.518 0.517 0.040 0.005 0.027 0.004
CSGvBM_ZC 0.498 0.498 0.473 0.501 0.499 0.497 0.022 0.132 0.007 0.472
CSILvBM_ZC 0.519 0.519 0.454 0.498 0.519 0.519 0.052 0.000 0.035 0.000
CSIvBM_ZC 0.507 0.507 0.459 0.500 0.508 0.507 0.029 0.042 0.021 0.030
CSLvBM_ZC 0.524 0.524 0.447 0.497 0.524 0.524 0.043 0.003 0.031 0.001
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),
)
_images/Macroeconomic cycles and asset class returns_180_0.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),
)
_images/Macroeconomic cycles and asset class returns_183_0.png
dix = dict_fxdu_rf

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="FX versus duration PnL across 23 markets",
    xcat_labels=[
        "based on cyclical strength z-score",
        "always long FX forward and paying 5-year IRS yields",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_184_0.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),
)
_images/Macroeconomic cycles and asset class returns_185_0.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",
)
display(df_eval.transpose())
Return (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl Traded Months
xcat
CSGILvBM_ZC_BIN 6.814016 10.0 0.681402 1.011889 -16.18766 -38.819127 0.035585 280
CSGILvBM_ZC_PZN 6.12695 10.0 0.612695 0.912501 -23.042983 -58.86435 0.069573 280
CSGIvBM_ZC_BIN 3.391267 10.0 0.339127 0.484229 -18.604428 -40.071032 0.028474 280
CSGIvBM_ZC_PZN 4.891006 10.0 0.489101 0.72162 -24.146526 -54.292016 0.031737 280
CSGLvBM_ZC_BIN 5.246954 10.0 0.524695 0.774486 -18.756393 -40.541743 0.043937 280
CSGLvBM_ZC_PZN 5.037258 10.0 0.503726 0.753619 -19.593715 -45.425978 0.05812 280
CSGvBM_ZC_BIN 2.982158 10.0 0.298216 0.436244 -13.265165 -23.875877 -0.007977 280
CSGvBM_ZC_PZN 3.032039 10.0 0.303204 0.446642 -19.760702 -36.383597 -0.006573 280
CSILvBM_ZC_BIN 4.600453 10.0 0.460045 0.667035 -19.402606 -43.348653 0.066938 280
CSILvBM_ZC_PZN 5.486882 10.0 0.548688 0.795458 -22.053283 -55.883856 0.090815 280
CSIvBM_ZC_BIN 2.297759 10.0 0.229776 0.327408 -18.755024 -32.476957 0.023697 280
CSIvBM_ZC_PZN 3.569855 10.0 0.356985 0.513916 -20.59831 -47.599124 0.050595 280
CSLvBM_ZC_BIN 5.241431 10.0 0.524143 0.768704 -17.560565 -46.550711 0.055286 280
CSLvBM_ZC_PZN 4.450189 10.0 0.445019 0.636002 -23.696623 -60.708569 0.096425 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)
)
_images/Macroeconomic cycles and asset class returns_187_0.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",
)
_images/Macroeconomic cycles and asset class returns_191_0.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,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="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
Panel 0.531 0.531 0.495 0.527 0.558 0.503 0.095 0.000 0.054 0.000
Mean years 0.532 0.514 0.511 0.529 0.540 0.488 0.025 0.416 0.026 0.430
Positive ratio 0.667 0.542 0.500 0.625 0.667 0.458 0.625 0.333 0.583 0.417
Mean cids 0.529 0.532 0.497 0.525 0.555 0.510 0.105 0.276 0.059 0.323
Positive ratio 0.680 0.880 0.520 0.760 0.880 0.440 0.920 0.800 0.840 0.760
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
CSGIL_ZC 0.531 0.531 0.495 0.527 0.558 0.503 0.095 0.000 0.054 0.000
CSGI_ZC 0.533 0.536 0.440 0.527 0.568 0.505 0.092 0.000 0.057 0.000
CSGL_ZC 0.531 0.529 0.532 0.527 0.555 0.504 0.095 0.000 0.055 0.000
CSG_ZC 0.525 0.527 0.458 0.527 0.556 0.498 0.092 0.000 0.057 0.000
CSIL_ZC 0.521 0.520 0.509 0.528 0.547 0.493 0.061 0.000 0.034 0.000
CSI_ZC 0.513 0.516 0.451 0.528 0.546 0.486 0.039 0.003 0.026 0.004
CSL_ZC 0.531 0.526 0.619 0.528 0.548 0.505 0.063 0.000 0.037 0.000
dix = dict_du52
srrx = dix["srr"]
srrx.accuracy_bars(
    type="years",
    # title="",
    size=(14, 6),
)
_images/Macroeconomic cycles and asset class returns_196_0.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),
)
_images/Macroeconomic cycles and asset class returns_199_0.png
dix = dict_du52

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="IRS curve flattening PnL across 25 markets",
    xcat_labels=[
        "based on negative of cyclical strength z-score",
        "always long 5-year versus 2-year, volatility neutral",
    ],
    ylab="% of risk capital, for 10% annualized long-term vol, no compounding",
    figsize=(16, 8),
)
_images/Macroeconomic cycles and asset class returns_200_0.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),
)
_images/Macroeconomic cycles and asset class returns_201_0.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 (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw USD_EQXR_NSA correl USD_DU05YXR_VT10 correl Traded Months
xcat
CSGIL_ZC_BIN 8.38964 10.0 0.838964 1.314304 -15.740757 -22.12074 0.015493 -0.07175 280
CSGIL_ZC_PZN 9.739004 10.0 0.9739 1.689592 -12.171555 -15.289467 0.015666 -0.073582 280
CSGI_ZC_BIN 8.330305 10.0 0.83303 1.316555 -13.589059 -20.966786 0.005349 -0.127655 280
CSGI_ZC_PZN 8.775674 10.0 0.877567 1.49381 -13.881762 -15.343638 0.021029 -0.108052 280
CSGL_ZC_BIN 8.547125 10.0 0.854713 1.287988 -20.285056 -26.899357 0.036539 0.045043 280
CSGL_ZC_PZN 10.111428 10.0 1.011143 1.731017 -14.579737 -15.880185 0.015041 -0.01956 280
CSG_ZC_BIN 7.511764 10.0 0.751176 1.15468 -14.129894 -17.64357 0.034542 -0.016362 280
CSG_ZC_PZN 9.534089 10.0 0.953409 1.616716 -17.489808 -12.685561 0.022357 -0.052247 280
CSIL_ZC_BIN 6.216466 10.0 0.621647 0.974137 -16.661005 -31.34704 0.006063 -0.103376 280
CSIL_ZC_PZN 7.581843 10.0 0.758184 1.25049 -17.902942 -30.900697 0.005549 -0.08094 280
CSI_ZC_BIN 3.677247 10.0 0.367725 0.556876 -16.9207 -31.288352 0.01575 -0.166196 280
CSI_ZC_PZN 4.533696 10.0 0.45337 0.705318 -18.796634 -33.928082 0.008996 -0.127238 280
CSL_ZC_BIN 8.128826 10.0 0.812883 1.228045 -16.904186 -25.261417 -0.005918 0.071882 280
CSL_ZC_PZN 8.918932 10.0 0.891893 1.448976 -16.997496 -21.774401 -0.003046 0.032646 280
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)
)
_images/Macroeconomic cycles and asset class returns_203_0.png