Classifying credit markets with macro factors #

Get packages and JPMaQS data #

import os
import numpy as np
import pandas as pd

from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB

from sklearn.metrics import make_scorer, balanced_accuracy_score

import macrosynergy.management as msm
import macrosynergy.panel as msp
import macrosynergy.pnl as msn
import macrosynergy.signal as mss
import macrosynergy.learning as msl
import macrosynergy.visuals as msv

from macrosynergy.download import JPMaQSDownload

import warnings

warnings.simplefilter("ignore")
# Cross-sections
cids_dm = ["USD", "EUR"]
cids_ig = ["UIG", "EIG"]
cids_hy = ["UHY", "EHY"]
cids_cr = ["UIG", "UHY", "EIG", "EHY"]

cids = cids_cr + cids_dm
# Indicator categories

rates = [
    "RYLDIRS02Y_NSA",
    "RYLDIRS05Y_NSA",
]

hpi = [
    "HPI_SA_P6M6ML6AR",
    "HPI_SA_P2Q2QL2AR",
    "HPI_SA_P1M1ML12",
    "HPI_SA_P1Q1QL4",
]

bsurv_changes = [
    # Construction confidence
    "CBCSCORE_SA_D6M6ML6",
    "CBCSCORE_SA_D3M3ML3",
    # Manufacturing confidence
    "MBCSCORE_SA_D6M6ML6",
    "MBCSCORE_SA_D3M3ML3",
    # Services confidence
    "SBCSCORE_SA_D6M6ML6",
    "SBCSCORE_SA_D3M3ML3",
]

csurv_changes = [
    "CCSCORE_SA_D6M6ML6",
    "CCSCORE_SA_D3M3ML3",
]

bank_lending = [
    "BLSCSCORE_NSA",
    "BLSCSCORE_NSA_D2Q2QL2",
    "BLSCSCORE_NSA_D1Q1QL4",
]

credit = [
    "PCREDITBN_SJA_P1M1ML12",
    "PCREDITBN_SJA_P1M1ML12_D1M1ML12",
]

main = rates + hpi + bsurv_changes + csurv_changes + bank_lending + credit

econ = [
    "INFTEFF_NSA",
    "INTRGDP_NSA_P1M1ML12_3MMA",
    "RGDP_SA_P1Q1QL4_20QMA",
]
mark = [
    "CRXR_VT10",
    "CRXR_NSA",
    "CRCRY_NSA",
]

xcats = main + econ + mark
# Tickers for download

single_tix = ["USD_GB10YXR_NSA", "USD_EQXR_NSA"]
tickers = (
    [cid + "_" + xcat for cid in cids_dm for xcat in main + econ]
    + [cid + "_" + xcat for cid in cids_cr for xcat in mark]
    + single_tix
)
# Download series from J.P. Morgan DataQuery by tickers

start_date = "2000-01-01"
end_date = None

# Retrieve credentials

oauth_id = os.getenv("DQ_CLIENT_ID")  # Replace with own client ID
oauth_secret = os.getenv("DQ_CLIENT_SECRET")  # Replace with own secret

# Download from DataQuery

downloader = JPMaQSDownload(client_id=oauth_id, client_secret=oauth_secret)
df = downloader.download(
    tickers=tickers,
    start_date=start_date,
    end_date=end_date,
    metrics=["value"],
    suppress_warning=True,
    show_progress=True,
)

dfd = df.copy()
dfd.info()
Downloading data from JPMaQS.
Timestamp UTC:  2025-10-27 14:35:43
Connection successful!
Requesting data: 100%|██████████| 3/3 [00:00<00:00,  4.93it/s]
Downloading data: 100%|██████████| 3/3 [00:13<00:00,  4.57s/it]
Some expressions are missing from the downloaded data. Check logger output for complete list.
4 out of 58 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()`.
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 348598 entries, 0 to 348597
Data columns (total 4 columns):
 #   Column     Non-Null Count   Dtype         
---  ------     --------------   -----         
 0   real_date  348598 non-null  datetime64[ns]
 1   cid        348598 non-null  object        
 2   xcat       348598 non-null  object        
 3   value      348598 non-null  float64       
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 10.6+ MB

Availability #

Renaming #

# Rename quarterly tickers to roughly equivalent monthly tickers

dict_repl = {
    "HPI_SA_P1Q1QL4": "HPI_SA_P1M1ML12",
    "HPI_SA_P2Q2QL2AR": "HPI_SA_P6M6ML6AR",
}

for key, value in dict_repl.items():
    dfd["xcat"] = dfd["xcat"].str.replace(key, value)
# Rename and duplicate economic area tickers in accordance with credit market tickers

dfa = dfd[
    (dfd["cid"].isin(["EUR", "USD"])) & ~(dfd["xcat"].isin(["EQXR_NSA", "GB10YXR_NSA"]))
]

dfa_ig = dfa.replace({"^EUR": "EIG", "^USD": "UIG"}, regex=True)
dfa_hy = dfa.replace({"^EUR": "EHY", "^USD": "UHY"}, regex=True)

dfx = pd.concat([dfd, dfa_ig, dfa_hy])

Check availability #

xcatx = rates + hpi
cidx = cids_cr

msm.check_availability(df=dfx, xcats=xcatx, cids=cidx, missing_recent=False)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/e0ebce480d5ed89f5cd13ab67a0244f6e1cc40afba9647ee9c3ac2da9cb1ea10.png
xcatx = bsurv_changes + csurv_changes + econ
cidx = cids_cr

msm.check_availability(df=dfx, xcats=xcatx, cids=cidx, missing_recent=False)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/eba4098b590b5370363153f994b0cfa1840b30253dd28dd8776e777960aba196.png
xcatx = bank_lending + credit
cidx = cids_cr

msm.check_availability(df=dfx, xcats=xcatx, cids=cidx, missing_recent=False)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/e90737c2d991621e240d30497dfd0f8d0ee5ac17927ed1fb7c0b763787a37492.png
xcatx = mark
cidx = cids_cr

msm.check_availability(df=dfx, xcats=xcatx, cids=cidx, missing_recent=False)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/0803ef66f9cc06ea4905d5aaf4d3882534b60131e270d46f194a7c217a701e41.png

Factor computation and checks #

Single-concept calculations #

factors = []

Business sentiment dynamics #

# Annualize sentiment score changes

cidx = cids_cr
calcs = []

bss = ["CBCSCORE_SA", "MBCSCORE_SA", "SBCSCORE_SA"]
for bs in bss:
    calcs.append(f"{bs}_D6M6ML6AR = {bs}_D6M6ML6 * 2")
    calcs.append(f"{bs}_D3M3ML3AR = {bs}_D3M3ML3 * 4")

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

bsar = [f"{bs}_D6M6ML6AR" for bs in bss] + [f"{bs}_D3M3ML3AR" for bs in bss]
cidx = ["UIG", "EIG"]
xcatx = bsar

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=1,
    same_y=True,
    aspect=3,
    title="US- and Euro-area business sentiment changes",
    xcat_labels=[
        "Construction survey changes, 6m/6m",
        "Manufacturing survey changes, 6m/6m",
        "Services survey changes, 6m/6m",
        "Construction survey changes, 3m/3m",
        "Manufacturing confidence changes, 3m/3m",
        "Services confidence changes, 3m/3m",
    ],
    cid_labels=["USD", "EUR"],
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/c0b6a26045acf48cdc3235cc608ed011e3cd90e2ab70c36bf6924f4b7e1e6d62.png
# Single business sentiment change
cidx = cids_cr
xcatx = bsar

dfa = msp.linear_composite(df=dfx, xcats=xcatx, cids=cids_cr, new_xcat="BCONFCHG")
dfx = msm.update_df(dfx, dfa)
cidx = cids_cr
xcatx = ["BCONFCHG"]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Business confidence dynamics score",
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/eb97c636a9de0a2a7d403de38c4e261e17ad069c4ae762e823ba8e6a2e149560.png

Real interest rate conditions #

# Real rates versus 3-year averages

cidx = cids_cr
calcs = []

for rate in rates:
    calcs.append(f"X{rate}_NEG = - {rate} + RGDP_SA_P1Q1QL4_20QMA")
    calcs.append(f"XX{rate}_NEG = - {rate} + 2")

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

xratns = [f"{x}{rate}_NEG" for rate in rates for x in ["X", "XX"]]
cidx = ["UIG", "EIG"]
xcatx = xratns

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="US- and Euro-area excess interest rates",
    ncol=1,
    same_y=True,
    aspect=3,
    xcat_labels=[
        "2-year real yields, excess of GDP growth, negative",
        "2-year real yields, excess of 2%, negative,",
        "5-year real yields, excess of GDP growth, negative",
        "5-year real yields, excess of 2%, negative",
    ],
    cid_labels=["USD", "EUR"],
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/44040a8ee07e2d9710998e78e5178c4278e838c8595254bf4300ef819983aaf9.png
# Composite excess real interest rate measure

xcatx = xratns
cidx = cids_cr

dfa = msp.linear_composite(df=dfx, xcats=xcatx, cids=cidx, new_xcat="XRATES_NEG")
dfx = msm.update_df(dfx, dfa)
cidx = cids_cr
xcatx = ["XRATES_NEG"]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Excess interest rates score",
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/52e5a87f80ca8e47be036e46a56781bf42fe14d10376c59fda193e6a9710572d.png

Bank lending surveys #

bls = ["BLSCSCORE_NSA", "BLSCSCORE_NSA_D2Q2QL2"]

cidx = ["UIG", "EIG"]
xcatx = bls

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="US- and Euro-area bank lending survey statistics",
    ncol=1,
    same_y=True,
    aspect=3,
    xcat_labels=["Bank lending survey levels", "Bank lending survey changes"],
    cid_labels=["USD", "EUR"],
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/ab85c4add10e16da105b49f97e1375c8940f22289f102849ac2b2b707ab59677.png
# Composite bank lending supply score
cidx = cids_cr
xcatx = bls

dfa = msp.linear_composite(df=dfx, xcats=xcatx, cids=cids_cr, new_xcat="BLSCOND")
dfx = msm.update_df(dfx, dfa)
cidx = cids_cr
xcatx = ["BLSCOND"]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Bank lending conditions score",
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/6ccfc20fe2491510f3ee7ec3ba80c9dccdf9fa3d787b4de98d3feed9e684474f.png

Private credit growth #

# Credit acceleration category

cidx = cids_cr
calcs = []

calcs.append("PCG_DOYA = PCREDITBN_SJA_P1M1ML12_D1M1ML12")
calcs.append("XPCG = PCREDITBN_SJA_P1M1ML12 - INFTEFF_NSA - RGDP_SA_P1Q1QL4_20QMA")

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

xpcgd = ["XPCG", "PCG_DOYA"]
cidx = ["UIG", "EIG"]
xcatx = xpcgd

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="US- and Euro-area private credit levels and changes",
    ncol=1,
    same_y=True,
    aspect=3,
    xcat_labels=["Excess private credit growth", "Private credit growth changes"],
    cid_labels=["USD", "EUR"],
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/61905589caa55f06ee35ea6a4b21a884abdb2dfd3f4e538e0486005e30ec39fa.png
# Single credit expansion category
cidx = cids_cr
xcatx = xpcgd

dfa = msp.linear_composite(df=dfx, xcats=xcatx, cids=cids_cr, new_xcat="XPCREDIT")
dfx = msm.update_df(dfx, dfa)
cidx = cids_cr
xcatx = ["XPCREDIT"]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Private credit score",
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/9ab647adc330801e7c8e8299631ecbc537dd9524c481bdd4d592294c81fa4999.png

CDS spread widening #

# Credit acceleration category (temp proxy)

cidx = cids_cr
calcs = []

calcs.append("CSPREAD_1MMA = CRCRY_NSA.rolling(21).mean()")
calcs.append("CSPREAD_3MMA = CRCRY_NSA.rolling(21*3).mean()")
calcs.append("CSPREAD_6MMA = CRCRY_NSA.rolling(21*6).mean()")
calcs.append("CSPREAD_3MMA_L1M = CSPREAD_3MMA.shift(21)")
calcs.append("CSPREAD_6MMA_L3M = CSPREAD_6MMA.shift(21*3)")
calcs.append(
    "CSPREAD_P1Mv3M_NEG = - ( CSPREAD_1MMA - CSPREAD_3MMA_L1M ) / CSPREAD_3MMA_L1M"
)
calcs.append(
    "CSPREAD_P3Mv6M_NEG = - ( CSPREAD_3MMA - CSPREAD_6MMA_L3M ) / CSPREAD_6MMA_L3M"
)

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

csp = ["CSPREAD_P1Mv3M_NEG", "CSPREAD_P3Mv6M_NEG"]
cidx = cids_cr
xcatx = csp

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="CDS spread widening, each credit index",
    ncol=2,
    same_y=True,
    aspect=1.5,
    xcat_labels=[
        "CDS spread widening, 1m vs 3m, negative",
        "CDS spread widening, 3m vs 6m, negative",
    ],
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/9c67e9450ba276f77735072de91746e88a4e2cf0b191e2ba84ebcd724e03437a.png
# Single credit expansion category
cidx = cids_cr
xcatx = csp

dfa = msp.linear_composite(
    df=dfx, xcats=xcatx, cids=cids_cr, new_xcat="CSPRWIDE_NEG", start="2002-01-01"
)
dfx = msm.update_df(dfx, dfa)
cidx = cids_cr
xcatx = ["CSPRWIDE_NEG"]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="CDS spread widening score",
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/32bf4524df2320d4cc4816f32a3f434c0f7e44796be664f02394a65baa474315.png

Transformations and visualizations #

factors = ["BCONFCHG", "XHPI", "XRATES_NEG", "BLSCOND", "XPCREDIT", "CSPRWIDE_NEG"]

Normalization #

# Zn-scores

xcatx = factors
cidx = cids_cr

dfa = pd.DataFrame(columns=list(dfx.columns))

for xc in xcatx:
    dfaa = msp.make_zn_scores(
        dfx,
        xcat=xc,
        cids=cidx,
        sequential=True,
        min_obs=261 * 3,
        neutral="zero",
        pan_weight=0,
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

# Modified factor dictionary

factorz = [xcat + "_ZN" for xcat in factors]

Visual checks #

xcatx = factorz
cidx = cids_cr

sdate = "2000-01-01"

renaming_dict = {
    "BCONFCHG_ZN": "Business confidence changes",
    "XHPI_ZN": "Excess house price inflation",
    "XRATES_NEG_ZN": "Excess real rates (negative)",
    "BLSCOND_ZN": "Bank lending survey score",
    "XPCREDIT_ZN": "Private credit expansion score",
    "CSPRWIDE_NEG_ZN": "Spread widening (negative)",
}

msp.correl_matrix(
    dfx,
    xcat_labels=renaming_dict,
    xcats=xcatx,
    cids=cidx,
    start=sdate,
    freq="M",
    title="Factor correlation since 2000, Euro and US markets",
    size=(14, 10),
    annot=True,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/18c9c81b0808ab4415f2ef6591ac9f90327215b2352de388f02cb35515197be3.png
cidx = cids_cr
xcatx = factorz[:3]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Business confidence, house prices and interest rates",
    xcat_labels=list(renaming_dict.values())[:3],
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/a445a6360d6bd35af51f324a613b5428f9e1f5803210f4e50b75a20b929907e9.png
cidx = cids_cr
xcatx = factorz[3:]

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    title="Bank lending surveys, private credit, credit spreads",
    xcat_labels=list(renaming_dict.values())[3:6],
    ncol=2,
    same_y=True,
    aspect=1.5,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/ce4c673f20d944d19316629d10bcd63e9447249f197510ac4153ac5ec8bc0929.png
crs = []

for xcat in factorz:
    cr = msp.CategoryRelations(
        df=dfx,
        xcats=[xcat, "CRXR_VT10"],
        cids=cids_hy,
        freq="M",
        lag=1,
        xcat_aggs=["last", "sum"],
        slip=1,
    )
    crs.append(cr)


msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=3,
    nrow=2,
    coef_box="lower right",
    prob_est="map",
    title="Conceptual factors and subsequent month cumulative high-yield CDS index returns, 10% vol-target",
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/ad2c9ac01e6a186c8ebb7efcbd80a59d8a35df64974ce5e875e0aca8021a75bf.png

Target return checks #

xcatx = ["CRXR_VT10"]
cidx = cids_cr

msp.view_timelines(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    cumsum=True,
    title="Cumulative returns for vol-targeted CDS indices ",
    ncol=2,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/0975ac88a38d061e3357bd1f62279bf2fdaa678dede301deff4ae8e953e686ca.png
xcatx = ["CRXR_VT10"]
cidx = cids_cr

msp.correl_matrix(df=dfx, xcats=xcatx, cids=cidx, cluster=True)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/b64b2bb2320e8f665defa69a2722d5dde861c245ad5485fbe1cc9d1872567a0d.png

Preparations for statistical learning #

Convert data to scikit-learn format (redundant) #

cidx = cids_cr
targ = "CRXR_VT10"
xcatx = factorz + [targ]

# Downsample from daily to monthly frequency (features as last and target as sum)
dfw = msm.categories_df(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
)

# Drop rows with missing values and assign features and target
dfw.dropna(inplace=True)
X_cr = dfw.iloc[:, :-1]
y_cr = dfw.iloc[:, -1]

Define cross-validation dynamics #

Visualize back test dynamics (with a one year forward window for visualization purposes).

  • The first set comprises the first 2 years of the panel.

  • A one month forward forecast is made.

Noting longer training times for random forests, that particular model is retrained every quarter.

# Initial split dynamics
min_cids = 1
min_periods = 12 * 2
test_size = 1

# Visualize back test pipeline
msl.ExpandingIncrementPanelSplit(
    train_intervals=test_size,
    test_size=12,
    min_cids=min_cids,
    min_periods=min_periods,
).visualise_splits(X_cr, y_cr)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/8c11fa6478b2df9cad6265402254dccaa38f1ebb5a7f976216c8bb5d8c513f84.png
# Cross-validation dynamics
splitters = {
    "Expanding": msl.ExpandingKFoldPanelSplit(4),
    "Rolling": msl.RollingKFoldPanelSplit(4),
}

split_functions = None
scorers = {"BAC": make_scorer(balanced_accuracy_score)}
cv_summary = "median"
splitters["Expanding"].visualise_splits(X_cr, y_cr)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/5aa32abe5ddfc4eb387d19b706a9fa70901afa96b671e5111adc7def6995c308.png
splitters["Rolling"].visualise_splits(X_cr, y_cr)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/075124aa8f428f4bcbf73e82aeed0b493761d72fe7495e78e700f483344f6b8a.png

Signal generation #

Pooled panel models #

Naive Bayes #

so_nb = msl.SignalOptimizer(
    df=dfx,
    xcats=factorz + ["CRXR_VT10"],
    cids=cids_cr,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    generate_labels=lambda x: 1 if x >= 0 else -1,
)

so_nb.calculate_predictions(
    name="NB",
    models={"NB": GaussianNB()},
    hyperparameters={
        "NB": {
            "var_smoothing": [1e-9, 1e-2, 1e-1, 5e-1, 1],
        },
    },
    scorers=scorers,
    inner_splitters=splitters,
    min_cids=min_cids,
    min_periods=min_periods,
    test_size=test_size,
    n_jobs_outer=-1,
    cv_summary=cv_summary,
    split_functions=split_functions,
)

so_nb.models_heatmap(
    "NB", title="Model selection heatmap, Naive Bayes panel classifier"
)

dfa = so_nb.get_optimized_signals("NB")
dfx = msm.update_df(dfx, dfa)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/2a6d79ebc36a0cae9894f7c923edbb0b63e3c2b7358012889b2acb38526204b9.png
msp.view_timelines(
    df=dfx,
    xcats=["NB"],
    cids=cids_cr,
    title="Out-of-sample Naive Bayes panel classifications",
    same_y=False,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/ef20bd6b414531427559bb7e21639fec535e83c7ed5d255310d03dbb6b09eb48.png

Logistic regression #

so_lr = msl.SignalOptimizer(
    df=dfx,
    xcats=factorz + ["CRXR_VT10"],
    cids=cids_cr,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    generate_labels=lambda x: 1 if x >= 0 else -1,
)

so_lr.calculate_predictions(
    name="LR",
    models={
        "LR": LogisticRegression(random_state=42),
    },
    hyperparameters={
        "LR": {
            "class_weight": ["balanced", None],
            "C": [1, 1e-2, 1e-4, 1e-6, 1e-8],
            "fit_intercept": [True, False],
        },
    },
    scorers=scorers,
    inner_splitters=splitters,
    min_cids=min_cids,
    min_periods=min_periods,
    cv_summary=cv_summary,
    test_size=test_size,
    n_jobs_outer=-1,
    split_functions=split_functions,
)

so_lr.models_heatmap(
    "LR", title="Model selection heatmap, Logistic Regression panel classifier"
)

dfa = so_lr.get_optimized_signals("LR")
dfx = msm.update_df(dfx, dfa)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/89efc7c79da5c5bfec44f766cc0495a94a8d0e3d4ed2e29121d090e2cdb8ac88.png
msp.view_timelines(
    df=dfx,
    xcats=["LR"],
    cids=cids_cr,
    title="Out-of-sample Logistic Regression panel classifications",
    same_y=False,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/e6315a1381f1a7e510e373dbe2c9d1352a4838204c166fb70de43f508b3bb492.png

K Nearest Neighbors #

so_knn = msl.SignalOptimizer(
    df=dfx,
    xcats=factorz + ["CRXR_VT10"],
    cids=cids_cr,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    generate_labels=lambda x: 1 if x >= 0 else -1,
)

so_knn.calculate_predictions(
    name="KNN",
    models={
        "KNN": msl.KNNClassifier(),
    },
    hyperparameters={
        "KNN": {
            "n_neighbors": ["sqrt", 0.1, 0.25],
            "weights": ["uniform", "distance"],
        },
    },
    scorers=scorers,
    inner_splitters=splitters,
    min_cids=min_cids,
    min_periods=min_periods,  # 2 years
    test_size=test_size,
    n_jobs_outer=-1,
    cv_summary=cv_summary,
)

so_knn.models_heatmap(
    "KNN", title="Model selection heatmap, Nearest Neighbors panel classifier"
)

dfa = so_knn.get_optimized_signals("KNN")
dfx = msm.update_df(dfx, dfa)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/e669b9262c21a90c08e8c1b30228cb87d7c395e4c23aedf0258a3e61bc975ab4.png
msp.view_timelines(
    df=dfx,
    title="Out-of-sample Nearest Neighbors panel classifications",
    xcats=["KNN"],
    cids=cids_cr,
    same_y=False,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/8680be2b3ea1e64f5129c5844bf6ae5870432e5a745885c0bd1625d299ff1ce4.png

Random forest #

so_rf = msl.SignalOptimizer(
    df=dfx,
    xcats=factorz + ["CRXR_VT10"],
    cids=cids_cr,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    generate_labels=lambda x: 1 if x >= 0 else -1,
)

so_rf.calculate_predictions(
    name="RF",
    models={
        "RF": RandomForestClassifier(
            random_state=42,
            n_estimators=500,
        ),
    },
    hyperparameters={
        "RF": {
            "class_weight": [None, "balanced"],
            "max_features": ["sqrt", None],
            "max_samples": [0.25, 0.1],
            "min_samples_leaf": [1, 5],
        },
    },
    scorers=scorers,
    inner_splitters=splitters,
    cv_summary=cv_summary,
    min_cids=min_cids,
    min_periods=min_periods,  # 2 years
    test_size=3,
    split_functions=split_functions,
    n_jobs_outer=-1,
)

so_rf.models_heatmap(
    "RF", title="Model selection heatmap, Random Forest panel classifier"
)

dfa = so_rf.get_optimized_signals("RF")
dfx = msm.update_df(dfx, dfa)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/b23c6c5d37d217e9468a5813325df6679e38eb2746b3489c04b3e629015d1285.png
msp.view_timelines(
    df=dfx,
    title="Out-of-sample Random Forest panel classifications",
    xcats=["RF"],
    cids=cids_cr,
    same_y=False,
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/b5688ac1912bb6667914e9734487cde39c7ef1b0a81d3382ff351bfa504498dd.png

Global signal #

For each of the signals previously calculated, we use majority voting to determine a single global signal that is applied to all cross-sections.

mods = ["NB", "LR", "KNN", "RF"]

for mod in mods:
    dfa = msp.panel_calculator(
        df=dfx,
        calcs=[f"{mod}_GLB = iEIG_{mod} + iEHY_{mod} + iUIG_{mod} + iUHY_{mod}"],
        cids=cids_cr,
    ).dropna()
    dfa["value"] = dfa["value"].apply(lambda x: 1 if x > 0 else -1 if x < 0 else 0)
    dfx = msm.update_df(dfx, dfa)

cidx = ["UIG"]
xcatx = [mod + "_GLB" for mod in mods]
msp.view_timelines(
    df=dfx,
    cids=cidx,
    xcats=xcatx,
    same_y=False,
    ncol=2,
    xcat_grid=True,
    title="Unified global signals based on various classification methods",
    xcat_labels=["Naive Bayes", "Logistic Regression", "Nearest Neighbors", "Random Forest"]
)
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/81f5920e10e1abe3397746a2080a7f8a65b637e910593467c2c874d282a0596c.png

Signal value checks #

Global signal #

Accuracy check #

## Compare optimized signals with simple average z-scores

srr = mss.SignalReturnRelations(
    df=dfx,
    rets=["CRXR_VT10"],
    sigs=["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"],
    cids=cids_cr,
    cosp=True,
    freqs=["M"],
    agg_sigs=["last"],
    start="2002-12-31",
    slip=1,
)

selcols = [
    "accuracy",
    "bal_accuracy",
    "pos_sigr",  # In the regime classification setting, this is less relevant
    "pos_retr",
]

srr.multiple_relations_table().round(3)  # [selcols]
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
CRXR_VT10 KNN_GLB M last 0.631 0.574 0.883 0.631 0.649 0.500 0.030 0.332 0.011 0.660 0.533
LR_GLB M last 0.603 0.518 0.821 0.643 0.649 0.388 0.031 0.321 0.012 0.624 0.512
NB_GLB M last 0.628 0.559 0.891 0.634 0.647 0.470 0.075 0.016 0.038 0.130 0.525
RF_GLB M last 0.642 0.568 0.907 0.646 0.659 0.476 0.047 0.135 0.050 0.045 0.525
xcatx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
for xcat in xcatx:
    srr.accuracy_bars(
        ret="CRXR_VT10",
        sigs=xcat,
        title=f"Monthly accuracy ratios for global signals based on {xcat}",
    )
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/bfa8b4cb6b1df8df8a93a12c7f101f4aa21405a0338e8eebac86f443183bdc6b.png https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/1fa29ed87ecdf13b2c24f2e1658eea12c18546eebd30c18fa6dbd39f39f2148d.png https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/83ad07ca59dbe268c86c42d7ff94456e0c2be17e97fb83023e8fc213819f9074.png https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/f11d537a59106cc62f9a9204fbd5dcd12adb760289b92f7dd06a5a7c1eaf087f.png

Naive PnL #

Simple notional positions #

sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
cidx = cids_ig

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_NSA",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0,
        sig_mult=1,
        rebal_freq="monthly",
        rebal_slip=1,
        # vol_scale=10,
    )

pnls.plot_pnls(
    title="Naive PnLs of long-short investment grade CDS strategy based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/1877360b2f07286e30c9801a15ef6a95326d431e06d9ef35a43a48577c6c8527.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB
Return % 1.204446 0.972137 1.022577 1.39137
St. Dev. % 3.862115 3.765793 3.712735 3.604383
Sharpe Ratio 0.311862 0.258149 0.275424 0.386022
Sortino Ratio 0.433861 0.365747 0.389853 0.54874
Max 21-Day Draw % -8.675489 -8.675489 -6.362013 -8.675489
Max 6-Month Draw % -11.285529 -10.279486 -11.378722 -10.984834
Peak to Trough Draw % -18.698017 -18.879914 -26.76123 -16.698009
Top 5% Monthly PnL Share 1.273024 1.604336 1.526501 1.084809
USD_GB10YXR_NSA correl -0.042963 0.015385 -0.0283 -0.108344
USD_EQXR_NSA correl 0.153867 0.085426 0.102716 0.255545
Traded Months 251 251 251 251
sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
cidx = cids_ig

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_NSA",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0.5,
        sig_mult=1,
        rebal_freq="monthly",
        neutral="zero",
        rebal_slip=1,
        # vol_scale=10,
    )

pnls.make_long_pnl(vol_scale=None, label="Long only")

pnls.plot_pnls(
    title="Naive PnLs of long-biased investment grade CDS indices based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
        "Risk Parity",
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx] + ["Long only"])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/bd5a57480bb5a3198b0df51910cc152110a6d209770fab344fa0df45e938fe92.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB Long only
Return % 2.300678 2.068369 2.118809 2.487601 2.193109
St. Dev. % 4.571479 4.314365 4.325368 4.645986 4.010422
Sharpe Ratio 0.503268 0.479414 0.489856 0.53543 0.546852
Sortino Ratio 0.705173 0.680858 0.702596 0.767121 0.780584
Max 21-Day Draw % -13.013234 -13.013234 -6.403795 -13.013234 -8.675489
Max 6-Month Draw % -13.878189 -10.181212 -12.056881 -10.181212 -11.587299
Peak to Trough Draw % -14.194191 -13.36372 -21.783944 -17.814142 -16.774382
Top 5% Monthly PnL Share 0.786852 0.827034 0.852895 0.778621 0.726715
USD_GB10YXR_NSA correl -0.159264 -0.116867 -0.154256 -0.205049 -0.280469
USD_EQXR_NSA correl 0.421339 0.383274 0.396093 0.484929 0.664532
Traded Months 251 251 251 251 251
sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
cidx = cids_hy

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_NSA",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0,
        sig_mult=1,
        rebal_freq="monthly",
        rebal_slip=1,
        # vol_scale=10,
    )

pnls.plot_pnls(
    title="Naive PnLs of long-short high-yield CDS strategy based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/541a00f8953d0065adc8df8c681dbd50e91423ba523c7fb92e72c901c62f49b4.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB
Return % 6.519865 5.235804 7.148311 8.396007
St. Dev. % 14.019362 13.89373 13.451977 13.604575
Sharpe Ratio 0.465061 0.376847 0.531395 0.617146
Sortino Ratio 0.645233 0.534488 0.760047 0.886921
Max 21-Day Draw % -39.639918 -39.639918 -26.586614 -39.639918
Max 6-Month Draw % -49.101137 -47.66054 -48.267141 -45.348528
Peak to Trough Draw % -62.150898 -73.973021 -77.609336 -47.372749
Top 5% Monthly PnL Share 0.893263 1.097063 0.839097 0.745394
USD_GB10YXR_NSA correl -0.080328 -0.016453 -0.062088 -0.122164
USD_EQXR_NSA correl 0.255057 0.168846 0.205341 0.340661
Traded Months 251 251 251 251
sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]

cidx = cids_hy

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_NSA",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0.5,
        sig_mult=1,
        rebal_freq="monthly",
        neutral="zero",
        rebal_slip=1,
    )

pnls.make_long_pnl(vol_scale=None, label="Long only")

pnls.plot_pnls(
    title="Naive PnLs of long-biased high-yield CDS indices based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
        "Risk Parity",
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx] + ["Long only"])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/1e3e12850c22ba545bde7b5f221277a70f313fb1cacf21364cd66143f1704d69.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB Long only
Return % 12.109053 10.824991 12.737499 13.985194 11.19603
St. Dev. % 17.858049 17.050265 17.266821 18.436973 14.801168
Sharpe Ratio 0.678073 0.634887 0.737686 0.758541 0.756429
Sortino Ratio 0.952389 0.907855 1.072287 1.097892 1.089497
Max 21-Day Draw % -59.459877 -59.459877 -29.327088 -59.459877 -39.639918
Max 6-Month Draw % -57.168804 -52.278336 -49.683214 -52.278336 -46.216449
Peak to Trough Draw % -63.390562 -63.327214 -71.68872 -60.820544 -58.497244
Top 5% Monthly PnL Share 0.626904 0.610265 0.610898 0.597408 0.594109
USD_GB10YXR_NSA correl -0.172263 -0.127782 -0.161311 -0.195917 -0.26363
USD_EQXR_NSA correl 0.491704 0.44287 0.461428 0.533694 0.703649
Traded Months 251 251 251 251 251

Vol-targeted positions #

sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
cidx = cids_cr

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_VT10",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0,
        sig_mult=1,
        rebal_freq="monthly",
        rebal_slip=1,
        # vol_scale=10,
    )

pnls.plot_pnls(
    title="Naive PnLs of long-short vol-targeted CDS strategy based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/5a20791bab4544b5de0e7f119a6f0ba05df2314b1ec271a9d606d2b89f56d7c1.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB
Return % 28.590534 19.996428 23.804768 26.736175
St. Dev. % 42.30784 42.204502 42.430608 42.513374
Sharpe Ratio 0.675774 0.473798 0.561028 0.628889
Sortino Ratio 0.94227 0.658362 0.771629 0.864943
Max 21-Day Draw % -131.548465 -131.548465 -99.790888 -131.548465
Max 6-Month Draw % -112.129873 -97.473066 -136.836688 -126.658224
Peak to Trough Draw % -135.27724 -161.496618 -159.610882 -175.858633
Top 5% Monthly PnL Share 0.548614 0.778274 0.678873 0.596437
USD_GB10YXR_NSA correl -0.113864 -0.053116 -0.09854 -0.136737
USD_EQXR_NSA correl 0.40306 0.30797 0.343069 0.417157
Traded Months 251 251 251 251
sigx = ["NB_GLB", "LR_GLB", "KNN_GLB", "RF_GLB"]
cidx = cids_cr

pnls = msn.NaivePnL(
    df=dfx,
    ret="CRXR_VT10",
    sigs=sigx,
    cids=cidx,
    start="2004-12-31",
    bms=["USD_GB10YXR_NSA", "USD_EQXR_NSA"],
)
for sig in sigx:
    pnls.make_pnl(
        sig=sig,
        sig_op="raw",
        sig_add=0.5,
        sig_mult=1,
        rebal_freq="monthly",
        neutral="zero",
        rebal_slip=1,
        # vol_scale=10,
    )

pnls.make_long_pnl(vol_scale=None, label="Long only")

pnls.plot_pnls(
    title="Naive PnLs of long-biased vol-targeted CDS strategy based on global classification",
    xcat_labels=[
        "Naive Bayes",
        "Logistic Regression",
        "Nearest Neighbors",
        "Random Forest",
        "Long only"
    ],
    title_fontsize=14,
)
pnls.evaluate_pnls(pnl_cats=["PNL_" + sig for sig in sigx] + ["Long only"])
https://macrosynergy.com/notebooks.build/strategies/classifying-credit-markets-with-macro-factors/_images/64a16073540a6bdf333766767607ec96ec190ba5d5414fbac3294b56f01769fd.png
xcat PNL_NB_GLB PNL_LR_GLB PNL_KNN_GLB PNL_RF_GLB Long only
Return % 43.616827 35.022721 38.831061 41.762468 30.079135
St. Dev. % 61.044937 58.532923 60.925891 61.945518 45.053268
Sharpe Ratio 0.714504 0.598342 0.637349 0.674181 0.667635
Sortino Ratio 0.991482 0.826753 0.87524 0.923929 0.916353
Max 21-Day Draw % -197.322698 -197.322698 -149.686332 -197.322698 -131.548465
Max 6-Month Draw % -155.153581 -146.209599 -205.255032 -193.352536 -136.836688
Peak to Trough Draw % -202.91586 -209.055207 -210.521833 -261.960049 -222.216642
Top 5% Monthly PnL Share 0.521162 0.62665 0.605556 0.538585 0.524118
USD_GB10YXR_NSA correl -0.164321 -0.12737 -0.154199 -0.178007 -0.231547
USD_EQXR_NSA correl 0.513708 0.46648 0.473745 0.517252 0.635395
Traded Months 251 251 251 251 251