Terms of trade as trading signals #

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

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

from IPython.display import display, Markdown
from macrosynergy.management.types import QuantamentalDataFrame

from macrosynergy.download import JPMaQSDownload
from IPython.display import HTML, display

import warnings

warnings.simplefilter("ignore")
# G3 and credit cross-sections

cids_g3 = ["EUR", "JPY", "USD"]

# FX cross-section lists

cids_dmsc_fx = [
    "AUD",
    "CAD",
    "CHF",
    "GBP",
    "NOK",
    "NZD",
    "SEK",
]  # DM small currency areas
cids_latm_fx = ["BRL", "COP", "CLP", "MXN", "PEN"]  # Latam
cids_emea_fx = ["CZK", "HUF", "ILS", "PLN", "RON", "RUB", "TRY", "ZAR"]  # EMEA
cids_emas_fx = ["IDR", "INR", "KRW", "MYR", "PHP", "THB", "TWD"] # Asia

cids_dmfx = cids_dmsc_fx
cids_emfx = cids_latm_fx + cids_emea_fx + cids_emas_fx
cids_fx = cids_dmfx + cids_emfx

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

# Equity-specific lists
cids_dmeq = ["AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "SEK", "USD"]
cids_emeq = ["BRL", "INR", "KRW", "MXN", "MYR", "PLN", "THB", "TRY", "TWD", "ZAR"]
cids_eq = cids_dmeq + cids_emeq
cids_eq.sort()

# Duration-specific lists
cids_dmdu = cids_dmeq + ["NOK", "NZD"]
cids_emdu = cids_emeq + ["CLP", "COP", "CZK", "HUF", "IDR", "ILS", "SGD"]
cids_du = cids_dmdu + cids_emdu

cids_eqdu = [cid for cid in cids_eq if cid in cids_du]


# Full set of cross-sections

cids = list(set(cids_fx) | set(cids_g3))
cids = list(np.sort(cids))
# Categories

ctots = [
    "CTOT_NSA_P1M1ML12",
    "CTOT_NSA_P1W4WL1",
    "CTOT_NSA_P1M12ML1",
    "CTOT_NSA_P1M60ML1",
]

xtra = [
   "REEROADJ_NSA_P1W4WL1", 
   "REEROADJ_NSA_P1M1ML12", 
   "REEROADJ_NSA_P1M12ML1", 
   "REEROADJ_NSA_P1M60ML1",
   "USDGDPWGT_SA_3YMA",
]

rets = [
   "FXTARGETED_NSA",  # Exchange rate target dummy
   "FXUNTRADABLE_NSA",  # Exchange rate untradable dummy
   "FXXR_NSA", # FX forward return, % of notional: dominant cross
   "FXXR_VT10",  # FX forward return for 10% vol target: dominant cross
   "EQXR_NSA",  # Equity index future returns in % of notional
   "EQXR_VT10",  # Equity index future return for 10% vol target
   "DU05YXR_VT10",  # Duration return, for 10% vol target: 5-year maturity
   "DU05YXR_NSA", # Duration return, 5-year maturity
  
]

xcats = ctots + xtra + rets

# Resultant tickers

tickers = [cid + "_" + xcat for cid in cids for xcat in xcats] 
print(f"Maximum number of tickers is {len(tickers)}")
Maximum number of tickers is 510
start_date = "2000-01-01"

# Retrieve credentials

client_id: str = os.getenv("DQ_CLIENT_ID")
client_secret: str = os.getenv("DQ_CLIENT_SECRET")

with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
    df = dq.download(
        tickers=tickers,
        start_date=start_date,
        suppress_warning=True,
        metrics=["all"],
        report_time_taken=True,
        show_progress=True,
    )
Downloading data from JPMaQS.
Timestamp UTC:  2025-10-06 14:26:51
Connection successful!
Requesting data: 100%|██████████| 128/128 [00:28<00:00,  4.45it/s]
Downloading data: 100%|██████████| 128/128 [00:49<00:00,  2.57it/s]
Time taken to download data: 	86.31 seconds.
Some expressions are missing from the downloaded data. Check logger output for complete list.
170 out of 2550 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()`.
dfx = df.copy().sort_values(["cid", "xcat", "real_date"])
dfx.info()
<class 'pandas.core.frame.DataFrame'>
Index: 3106836 entries, 0 to 3106835
Data columns (total 8 columns):
 #   Column        Dtype         
---  ------        -----         
 0   real_date     datetime64[ns]
 1   cid           object        
 2   xcat          object        
 3   value         float64       
 4   grading       float64       
 5   eop_lag       float64       
 6   mop_lag       float64       
 7   last_updated  datetime64[ns]
dtypes: datetime64[ns](2), float64(4), object(2)
memory usage: 213.3+ MB
xcatx = ctots 
msm.check_availability(dfx, xcats=xcatx, cids=cids, missing_recent=False), 
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/6a7ce3866817c7d763c562134530871d97070b40519217f17b56e99206bc5be5.png
(None,)
dfb = df[df["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
    :, ["cid", "xcat", "real_date", "value"]
]
dfba = (
    dfb.groupby(["cid", "real_date"])
    .aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
    .reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
{'BRL': (Timestamp('2012-12-03 00:00:00'), Timestamp('2013-09-30 00:00:00')),
 'CHF': (Timestamp('2011-10-03 00:00:00'), Timestamp('2015-01-30 00:00:00')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
 'ILS': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-12-30 00:00:00')),
 'INR': (Timestamp('2000-01-03 00:00:00'), Timestamp('2004-12-31 00:00:00')),
 'MYR_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2007-11-30 00:00:00')),
 'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2025-10-03 00:00:00')),
 'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
 'RON': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2025-10-03 00:00:00')),
 'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
 'TRY_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2003-09-30 00:00:00')),
 'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2024-07-31 00:00:00'))}

Transformations and setups #

Features #

Adjusted terms of trade changes #

# Check for positive relation

cidx = cids_fx

dict_looks = {
    "P1W4WL1": "% latest week over previous 4 weeks",
    "P1M12ML1": "% latest month over 12 months",
    "P1M1ML12": "% latest month over a year ago",
    "P1M60ML1": "% latest month over previous 5 years",
}

cr = {
    k: msp.CategoryRelations(
        dfx,
        xcats=["CTOT_NSA_" + k, "REEROADJ_NSA_" + k],
        cids=cidx,
        freq="Q",
        lag=0,
        xcat_aggs=["mean", "mean"],
        start="2000-01-01",
    )
    for k in dict_looks.keys()
}

cat_rels = [cr[k] for k in dict_looks.keys()]
subtitles = [v for v in dict_looks.values()]

msv.multiple_reg_scatter(
    cat_rels=cat_rels,
    title="Terms of trade changes versus real effective appreciation, 27 smaller countries, since 2000",
    title_fontsize=18,
    xlab="Commodity-based terms-of-trade change,  quarter average",
    ylab="Openness-adjusted effective real appreciation, quarter average",
    ncol=2,
    nrow=2,
    figsize=(12, 8),
    prob_est="map",
    coef_box=None,
    subplot_titles=subtitles,
    share_axes=False,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/6e1868a831ef7148af00a9822c88d8a93e1a510b21d9a9d192b782ae2d894aca.png
# Normalized changes

base_cats =["CTOT_NSA", "REEROADJ_NSA"]
xcatx = [b + "_" + k for b in base_cats for k in dict_looks.keys()]

for xcat in xcatx:
    dfa = msp.make_zn_scores(
        dfx,
        xcat=xcat,
        cids=cids,
        sequential=True,
        min_obs=261 * 5,
        neutral="zero",
        pan_weight=0,  # very different panels  
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfx = msm.update_df(dfx, dfa)
# Inspection

chg = "P1M1ML12"  
xcatx = [b + "_" + chg + "_ZN" for b in base_cats]
cidx = cids_fx

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/d7e6d9616ed040c645460ddf5d4ce443daf210ea907245d2794011ac4d1fc745.png
# Full and half adjustment of terms of trade changes

cidx = cids

calcs = []
dict_weights = {"_FA": 1, "_HA": 0.5}

for k, v in dict_weights.items():
    for chg in dict_looks.keys():
        calcs += [f"CTOT_NSA_{chg}_ZN{k} = CTOT_NSA_{chg}_ZN - REEROADJ_NSA_{chg}_ZN * {v}"]

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

chg = "P1M1ML12"  
xcatx = [
    f"CTOT_NSA_{chg}_ZN",
    f"REEROADJ_NSA_{chg}_ZN",
    f"CTOT_NSA_{chg}_ZN_FA",
]  
cidx = cids_fx[:1]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=1,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/6d10d22d257c8e3070fdb607554ada3136468bc18416bb7b7717619086a44162.png
# Inspection

chg = "P1M1ML12"  
xcatx = [f"CTOT_NSA_{chg}_ZN" , f"CTOT_NSA_{chg}_ZN_HA" , f"CTOT_NSA_{chg}_ZN_FA"]
cidx = cids_fx

msp.view_ranges(
    dfx,
    xcats=xcatx,
    cids=cidx,
    sort_cids_by="mean",
    size=(12, 5)
)

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/5a46058529841bbe282b947fc5c5aeaa5a6d2ef189e69b6d0b782e1a841ae1aa.png https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/04e83c9a4899d8414b5eaec147ab06667ea9f071acbf7ce5bd568691eef1b6cd.png

Multi-horizon terms of trade changes #

# Governance dictionary of multi-horizon changes

ads = ["", "_HA", "_FA"]
chgs = [k for k in dict_looks.keys()]
dict_horizons = {f"CTOT_NSA_CHG_ZN{ad}": [f"CTOT_NSA_{chg}_ZN{ad}" for chg in chgs] for ad in ads}

cidx = cids

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

for new, xcatx in dict_horizons.items():
    dfaa = msp.linear_composite(
        df=dfx,
        xcats=xcatx,
        cids=cidx,
        complete_xcats=False,
        new_xcat=new,
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

multis = list(dict_horizons.keys())
# Inspection

multis_labs = {
    "CTOT_NSA_CHG_ZN": "Multi-horizon terms of trade change score",
    "CTOT_NSA_CHG_ZN_FA": "Multi-horizon terms of trade change score, adjusted for concurrent real effective appreciation",
}

xcatx = ['CTOT_NSA_CHG_ZN', 'CTOT_NSA_CHG_ZN_FA']
cidx = cids

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    aspect=1.8,
    height=1.6,
    start="2000-01-01",
    same_y=True,
    title="Multi-horizon terms of trade change scores (without and with REER adjustment)",
    title_fontsize=24,
    xcat_labels=multis_labs,
    legend_fontsize=16,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/126537e883c3885a72f83f041cd49c153f5b19b020f66a7db269a28488cd298f.png

Group terms of trade changes #

# All terms of trade changes

chgs = [k for k in dict_looks.keys()]
all_tots = []
for chg in chgs:
    all_tots.extend([f"CTOT_NSA_{chg}_ZN", f"CTOT_NSA_{chg}_ZN_HA", f"CTOT_NSA_{chg}_ZN_FA"])

all_tots.extend(multis)
# Global weighted composites of changes metrics

xcatx = all_tots

dict_globals = {"GLED": cids_eq}

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

for k, v in dict_globals.items():
    for xc in xcatx:
        dfaa = msp.linear_composite(
            dfx,
            xcats=[xc],
            cids=v,
            weights="USDGDPWGT_SA_3YMA",  # USD GDP weights
            new_cid=k,
        )
        dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

Relative terms-of-trade changes #

Equity #
# Custom blacklist for equity

xcatx = ["EQXR_NSA"]


dfxx = dfx[dfx["xcat"].isin(xcatx) & dfx["cid"].isin(cidx)]

dfxx_m = (
    dfxx.groupby(["cid", "xcat"], as_index=True)              
       .resample("M", on="real_date")
       .agg(value=("value", "sum"))                          
       .reset_index()                                         
)

calcs = [f"EQ_BLACK = {xcatx[0]} / {xcatx[0]} - 1"] # returns zero if data present
dfa = msp.panel_calculator(dfxx_m, cids=cidx, calcs=calcs)
eq_black = msp.make_blacklist(dfa, "EQ_BLACK", nan_black=True) # blacklist if no data to avoid inclusion in relative basket calculations

eq_black
{'AUD': (Timestamp('2000-01-31 00:00:00'), Timestamp('2000-04-30 00:00:00')),
 'INR': (Timestamp('2000-01-31 00:00:00'), Timestamp('2000-08-31 00:00:00')),
 'PLN': (Timestamp('2000-01-31 00:00:00'), Timestamp('2013-08-31 00:00:00')),
 'SEK': (Timestamp('2000-01-31 00:00:00'), Timestamp('2005-01-31 00:00:00')),
 'THB': (Timestamp('2000-01-31 00:00:00'), Timestamp('2006-04-30 00:00:00')),
 'TRY': (Timestamp('2000-01-31 00:00:00'), Timestamp('2005-01-31 00:00:00'))}
# Relative terms of trade changes versus equity baskets

xcatx = all_tots
cidx = cids_eq

region_map = {"vGEQ": cids_eq}

for postfix, cidx in region_map.items():
    dfa = msp.make_relative_value(
        dfx,
        xcats=xcatx,
        cids=cidx,
        start="2000-01-01",
        rel_meth="subtract",
        complete_cross=False,
        blacklist=eq_black,
        postfix=postfix,
    )
    dfx = msm.update_df(dfx, dfa)
# Inspection

chg = "CHG"  # 'P1W4WL1', 'P1M1ML12', 'P1M12ML1', 'P1M60ML1' 'CHG'
xcatx = [f"CTOT_NSA_{chg}_ZN" , f"CTOT_NSA_{chg}_ZNvGEQ"]
cidx = cids_eq

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/963238b8978826890b1c72dc69d992f860b339a9284d5ebf2503407db63db7eb.png
Equity duration risk parity #
# Custom blacklist for equity duration

cidx = cids_eqdu
xcatx = ["EQXR_VT10", "DU05YXR_VT10"]

dfxx = dfx[dfx["xcat"].isin(xcatx) & dfx["cid"].isin(cidx)]

# use monthly resumplig to reduce calculation time for blacklist creation
dfxx_m = (
    dfxx.groupby(["cid", "xcat"])
    .resample("M", on="real_date")
    .agg(value=("value", "sum"))   
    .reset_index()
) 

calcs = [f"EQDU_BLACK = {xcatx[0]} / {xcatx[0]} + {xcatx[1]} / {xcatx[1]} - 2"] # returns zero if data present for both equity and duration returns
dfa = msp.panel_calculator(dfxx_m, cids=cidx, calcs=calcs)
eqdu_black = msp.make_blacklist(dfa, "EQDU_BLACK" ,nan_black=True)
eqdu_black
{'AUD': (Timestamp('2000-01-31 00:00:00'), Timestamp('2001-06-30 00:00:00')),
 'BRL': (Timestamp('2000-01-31 00:00:00'), Timestamp('2008-07-31 00:00:00')),
 'INR': (Timestamp('2000-01-31 00:00:00'), Timestamp('2006-04-30 00:00:00')),
 'KRW': (Timestamp('2000-01-31 00:00:00'), Timestamp('2006-04-30 00:00:00')),
 'MXN': (Timestamp('2000-01-31 00:00:00'), Timestamp('2005-12-31 00:00:00')),
 'MYR': (Timestamp('2000-01-31 00:00:00'), Timestamp('2005-12-31 00:00:00')),
 'PLN': (Timestamp('2000-01-31 00:00:00'), Timestamp('2013-08-31 00:00:00')),
 'SEK_1': (Timestamp('2000-01-31 00:00:00'), Timestamp('2005-01-31 00:00:00')),
 'SEK_2': (Timestamp('2008-04-30 00:00:00'), Timestamp('2008-05-31 00:00:00')),
 'THB': (Timestamp('2000-01-31 00:00:00'), Timestamp('2006-04-30 00:00:00')),
 'TRY': (Timestamp('2000-01-31 00:00:00'), Timestamp('2009-07-31 00:00:00')),
 'TWD': (Timestamp('2000-01-31 00:00:00'), Timestamp('2006-04-30 00:00:00')),
 'ZAR': (Timestamp('2000-01-31 00:00:00'), Timestamp('2001-05-31 00:00:00'))}
Relative values for FX countries #
# Relative values for FX countries

cidx = cids_fx
xcatx = all_tots

dfa = msp.make_relative_value(
    dfx,
    xcats=xcatx,
    cids=cidx,
    blacklist=fxblack,
    complete_cross=False,
    postfix="vGFX",
)

dfx = msm.update_df(dfx, dfa)
# Inspection

chg = "CHG"  
xcatx = [f"CTOT_NSA_{chg}_ZN" , f"CTOT_NSA_{chg}_ZNvGFX"]
cidx = cids_fx

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/64e59e40782466f4ea433323c9f03556af342cf69e4b950183d14e13406d0d7c.png

Targets #

Additonal directional returns #

# Risk parity returns

cidx = cids_eqdu

calc_edc = ["EQDUXR_RP = EQXR_VT10 + DU05YXR_VT10",
            "EQvDUXR_RP = EQXR_VT10 - DU05YXR_VT10"]

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

# Vol estimation of long-long risk parity positions

dfa = msp.historic_vol(
    dfx, xcat="EQDUXR_RP", cids=cidx, lback_meth="xma", postfix="_ASD"
)
dft = dfa.pivot(index="real_date", columns="cid", values="value")
dftx = dft.resample("BM").last().reindex(dft.index).ffill().shift(1)
dfax = dftx.unstack().reset_index().rename({0: "value"}, axis=1)
dfax["xcat"] = "EQDUXR_RP_ASDML1"
dfx = msm.update_df(dfx, dfax)

# Vol estimation of long-short risk parity positions

dfa = msp.historic_vol(
    dfx, xcat="EQvDUXR_RP", cids=cidx, lback_meth="xma", postfix="_ASD"
)
dft = dfa.pivot(index="real_date", columns="cid", values="value")
dftx = dft.resample("BM").last().reindex(dft.index).ffill().shift(1)
dfax = dftx.unstack().reset_index().rename({0: "value"}, axis=1)
dfax["xcat"] = "EQvDUXR_RP_ASDML1"
dfx = msm.update_df(dfx, dfax)

# Vol-target risk parity returns

calc_vaj = [
    "EQDUXR_RPVT10 = 10 * EQDUXR_RP / EQDUXR_RP_ASDML1",
    "EQvDUXR_RPVT10 = 10 * EQvDUXR_RP / EQvDUXR_RP_ASDML1",
]
dfa = msp.panel_calculator(dfx, calcs=calc_vaj, cids=cidx)
dfx = msm.update_df(dfx, dfa)
# Inspection

xcatx = ["EQvDUXR_RPVT10", "EQvDUXR_RP"] 
cidx = cids_eqdu

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    cumsum=True,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=False,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/203084797f6068a80797250f0744fefa00dcb392ba1ef7b08a73a9cc3d222faa.png

Basket returns #

# Global basket proxy returns

dict_brets = {"GLED": [cids_eq, eqdu_black, ["EQDUXR_RP", "EQDUXR_RPVT10", "EQvDUXR_RP", "EQvDUXR_RPVT10"]]}

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

for cid, params in dict_brets.items():
    cidx, black, xcatx = params
    for xc in xcatx:
        dfaa = msp.linear_composite(
            dfx,
            xcats=[xc],
            blacklist=black,
            cids=cidx,
            new_cid=cid,
        )
        dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

brets = [k + "_" + v[2][0] for k, v in dict_brets.items()]
brets_vt = [k + "_" + v[2][1] for k, v in dict_brets.items()]
# Visualize new global aggregates

gcid = "GLED"
xcatx = dict_brets[gcid][2]

dict_xcat_labs = {
    "EQDUXR_RP": "Risk parity equity duration, % local-currency return",
    "EQDUXR_RPVT10": "Risk parity equity duration return, 10% vol target",
    "EQvDUXR_RP": "Long-short risk parity equity duration return, % local-currency",
    "EQvDUXR_RPVT10": "Long-short risk parity equity duration return, 10% vol target",
}


msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=[gcid],
    cumsum=True,
    xcat_grid=True,
    ncol=2,
    start="2000-01-01",
    size=(10, 5),
    xcat_labels=dict_xcat_labs,
    title="GDP-weighted global directional returns",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/f1efb3c60d8bacfd10839bdc28074625ddfe26dbe44e7faca668182b2757b8b1.png

Relative vol-targeted returns #

# Approximate relative vol-targeted returns

dict_rels = {
    "EQXR_VT10": [(cids_eq,   eq_black,  "vGEQ")],
    "FXXR_VT10": [(cids_fx, fxblack, "vGFX")],
}

dfa = pd.DataFrame(columns=list(dfx.columns))
for ret, param_list in dict_rels.items():
    for cids_ac, blacklist, postfix in param_list:
        dfaa = msp.make_relative_value(
            dfx, xcats=[ret], cids=cids_ac, postfix=postfix, blacklist=blacklist
        )
        dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)
xcatx = ["EQDUXR_RPVT10", ] 
cidx = cids_eq

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    cumsum=True,
    ncol=4,
    aspect=2,
    height=1.8,
    start="2000-01-01",
    same_y=False,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/22160700b573b097511f98fcd4df95bede16b877425b316f4c74f5e8602fd90a.png

Value checks #

Directional FX #

Specs and panel test #

# Basic specs

dict_fxd = {
    "sigs": [
        "CTOT_NSA_CHG_ZN",
        "CTOT_NSA_P1W4WL1_ZN",
        "CTOT_NSA_P1M12ML1_ZN",
        "CTOT_NSA_P1M60ML1_ZN",
    ],
    "targ": "FXXR_VT10",
    "cids": cids_fx,
    "start": "2000-01-01",
    "black": fxblack,
    "crs": None,
    "srr": None,
    "pnls": None,
}
# Panel correlation test

dix = dict_fxd
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

# Store correlation results

crs = [
    msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="m",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )
    for sig in sigx
]

dix["crs"] = crs
# Display tests

dix = dict_fxd
crs = dix['crs']

msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=2,
    nrow=2,
    figsize=(12, 12),
    prob_est="map",
    coef_box="lower left",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/bee939747f1960e55af5bf35b30c0b6e772fccadeddf78ff05fd9227a0872d0b.png

Accuracy and correlation check #

# Signal-return relations object and results

dix = dict_fxd
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    sigs=sigx,
    cosp=True,
    rets=targ,
    freqs=["M"],
    blacklist=blax,
    slip=1
)

display(srr.signals_table().sort_index().astype("float").round(3).T)

dix["srr"] = srr
Return FXXR_VT10
Signal CTOT_NSA_CHG_ZN CTOT_NSA_P1M12ML1_ZN CTOT_NSA_P1M60ML1_ZN CTOT_NSA_P1W4WL1_ZN
Frequency M M M M
Aggregation last last last last
accuracy 0.511 0.507 0.507 0.514
bal_accuracy 0.514 0.509 0.513 0.514
pos_sigr 0.466 0.475 0.432 0.490
pos_retr 0.541 0.541 0.541 0.541
pos_prec 0.556 0.551 0.555 0.555
neg_prec 0.473 0.468 0.471 0.474
pearson 0.029 0.025 0.020 0.022
pearson_pval 0.010 0.028 0.071 0.050
kendall 0.018 0.015 0.013 0.016
kendall_pval 0.014 0.040 0.072 0.036
auc 0.514 0.510 0.513 0.515
# Accuracy bars

dix = dict_fxd
srr = dix["srr"]
sigs = dix["sigs"]

srr.accuracy_bars(
    type="signals", sigs=sigs, title=None, size=(16, 5), title_fontsize=20
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/6e505b9deac90679603950d38e93743951bfe0073ea711c6f87518c9bfb9028d.png

Naive PnL #

dix = dict_fxd
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

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

for sig in list(sigx):
    naive_pnl.make_pnl(
        sig,
        sig_add=0,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="weekly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

naive_pnl.make_long_pnl(label="Long only", vol_scale=10)
dix["pnls"] = naive_pnl
dix = dict_fxd
sigx = dix["sigs"]
cidx = dix["cids"]
start = dix["start"]
naive_pnl = dix["pnls"]

pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    title_fontsize=16,
    figsize=(16, 8),
    compounding=False,
    xcat_labels=None,
)

display(naive_pnl.evaluate_pnls(pnl_cats=pnls).astype("float").round(2))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/c1cfb854cb292768e833ed7c2a8188c465642ba64467941ffb7f8f4efe04b9e8.png
xcat CTOT_NSA_CHG_ZN_PZN CTOT_NSA_P1W4WL1_ZN_PZN CTOT_NSA_P1M12ML1_ZN_PZN CTOT_NSA_P1M60ML1_ZN_PZN
Return % 3.72 2.29 4.10 2.51
St. Dev. % 10.00 10.00 10.00 10.00
Sharpe Ratio 0.37 0.23 0.41 0.25
Sortino Ratio 0.54 0.33 0.59 0.36
Max 21-Day Draw % -13.18 -13.61 -16.94 -15.76
Max 6-Month Draw % -19.32 -17.98 -25.33 -23.62
Peak to Trough Draw % -35.28 -45.24 -38.11 -34.70
Top 5% Monthly PnL Share 1.40 1.91 1.21 1.94
EUR_FXXR_NSA correl 0.03 -0.01 0.01 0.04
USD_EQXR_NSA correl -0.01 -0.02 -0.02 0.01
USD_DU05YXR_NSA correl 0.00 0.02 -0.00 -0.02
Traded Months 310.00 310.00 310.00 310.00

Directional equity strategy #

Specs and panel test #

# Basic specs

dict_deq = {
    "sigs": [
        "CTOT_NSA_CHG_ZN",
        "CTOT_NSA_P1M12ML1_ZN",
        "CTOT_NSA_CHG_ZN_HA",
        "CTOT_NSA_CHG_ZN_FA",
    ],
    "targ": "EQXR_NSA",
    "cids": cids_eq,
    "start": "2000-01-01",
    "black": eq_black,
    "crs": None,
    "srr": None,
    "pnls": None,
}
# Panel correlation test

dix = dict_deq
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

# Store correlation results

crs = [
    msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="m",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )
    for sig in sigx
]

dix["crs"] = crs
# Display tests

dix = dict_deq
crs = dix['crs']

msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=2,
    nrow=2,
    figsize=(12, 12),
    prob_est="map",
    coef_box="lower left",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/f30fe4a61a5b0fb3f1c87e6fdf64a68dfb93eec316b11ef26990ce1c728ff15f.png

Accuracy and correlation check #

# Signal-return relations object and results

dix = dict_deq
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    sigs=sigx,
    cosp=True,
    rets=targ,
    freqs=["M"],
    blacklist=blax,
    slip=1
)

display(srr.signals_table().sort_index().astype("float").round(3).T)

dix["srr"] = srr
Return EQXR_NSA
Signal CTOT_NSA_CHG_ZN CTOT_NSA_CHG_ZN_FA CTOT_NSA_CHG_ZN_HA CTOT_NSA_P1M12ML1_ZN
Frequency M M M M
Aggregation last last last last
accuracy 0.515 0.513 0.512 0.504
bal_accuracy 0.521 0.518 0.518 0.511
pos_sigr 0.466 0.471 0.467 0.463
pos_retr 0.584 0.584 0.584 0.584
pos_prec 0.607 0.604 0.603 0.596
neg_prec 0.435 0.433 0.433 0.426
pearson 0.037 0.046 0.045 0.031
pearson_pval 0.009 0.001 0.001 0.026
kendall 0.021 0.025 0.024 0.015
kendall_pval 0.028 0.008 0.010 0.099
auc 0.522 0.519 0.518 0.511
# Accuracy bars

dix = dict_deq
srr = dix["srr"]
sigs = dix["sigs"]

srr.accuracy_bars(
    type="signals", sigs=sigs, title=None, size=(16, 5), title_fontsize=20
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/75e254bad575d5dac698d9b388e62b9a3a459ba77d962d548bb9b424f076fe31.png

Naive PnL #

dix = dict_deq
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

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

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

naive_pnl.make_long_pnl(label="Long only", vol_scale=10)
dix["pnls"] = naive_pnl
dix = dict_deq
sigx = dix["sigs"]
cidx = dix["cids"]
start = dix["start"]
naive_pnl = dix["pnls"]

pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    title_fontsize=16,
    figsize=(16, 8),
    compounding=False,
    xcat_labels=None,
)

display(naive_pnl.evaluate_pnls(pnl_cats=pnls).astype("float").round(2))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/000bf276e96b8aab3796286e728f9b51a3c9c48ce2891622ba3a8cad76248474.png
xcat CTOT_NSA_CHG_ZN_PZN CTOT_NSA_P1M12ML1_ZN_PZN CTOT_NSA_CHG_ZN_HA_PZN CTOT_NSA_CHG_ZN_FA_PZN
Return % 2.61 2.24 2.90 3.08
St. Dev. % 10.00 10.00 10.00 10.00
Sharpe Ratio 0.26 0.22 0.29 0.31
Sortino Ratio 0.38 0.33 0.42 0.45
Max 21-Day Draw % -23.46 -16.92 -24.77 -23.49
Max 6-Month Draw % -20.63 -16.45 -21.04 -29.01
Peak to Trough Draw % -33.43 -33.38 -39.34 -51.43
Top 5% Monthly PnL Share 1.92 2.26 1.73 1.51
EUR_FXXR_NSA correl -0.05 -0.01 -0.01 0.03
USD_EQXR_NSA correl 0.00 0.04 0.08 0.14
USD_DU05YXR_NSA correl 0.03 0.02 -0.00 -0.02
Traded Months 310.00 310.00 310.00 310.00

Directional global equity duration strategy #

Specs and panel test #

# Basic specs

dict_ged = {
    "sigs": [
        "CTOT_NSA_P1W4WL1_ZN",
        "CTOT_NSA_P1W4WL1_ZN_FA",
    ],
    "targ": "EQDUXR_RP",
    "cids": ["GLED"],
    "start": "2000-01-01",
    "black": eq_black,
    "crs": None,
    "srr": None,
    "pnls": None,
}
# Panel correlation test

dix = dict_ged
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

# Store correlation results

crs = [
    msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="m",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )
    for sig in sigx
]

dix["crs"] = crs
# Display tests

dix = dict_ged
crs = dix['crs']

msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=2,
    nrow=1,
    figsize=(14, 8),
    prob_est="map",
    coef_box="lower left",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/bad4649dcacf2aadc1f28e130ef95078f201773853789458c1783bff54121073.png

Accuracy and correlation check #

# Signal-return relations object and results

dix = dict_ged
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    sigs=sigx,
    cosp=True,
    rets=targ,
    freqs=["M"],
    blacklist=blax,
    slip=1
)

display(srr.signals_table().astype("float").round(3).T)

dix["srr"] = srr
Return EQDUXR_RP
Signal CTOT_NSA_P1W4WL1_ZN CTOT_NSA_P1W4WL1_ZN_FA
Frequency M M
Aggregation last last
accuracy 0.508 0.496
bal_accuracy 0.511 0.497
pos_sigr 0.487 0.491
pos_retr 0.587 0.587
pos_prec 0.598 0.584
neg_prec 0.423 0.410
pearson 0.045 0.019
pearson_pval 0.002 0.176
kendall 0.028 0.014
kendall_pval 0.004 0.142
auc 0.511 0.497
# Accuracy bars

dix = dict_ged
srr = dix["srr"]
sigs = dix["sigs"]

srr.accuracy_bars(
    type="signals", sigs=sigs, title=None, size=(16, 5), title_fontsize=20
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/2e174e4e23e52ff17822bb3b45d6e11be1f1a3dd524d49ce0ef048861c53292e.png

Naive PnL #

dix = dict_ged
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

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

for sig in list(sigx):
    naive_pnl.make_pnl(
        sig,
        sig_add=0,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

naive_pnl.make_long_pnl(label="Long only", vol_scale=10)
dix["pnls"] = naive_pnl
dix = dict_ged
sigx = dix["sigs"]
cidx = dix["cids"]
start = dix["start"]
naive_pnl = dix["pnls"]

pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    title_fontsize=16,
    figsize=(16, 8),
    compounding=False,
    xcat_labels=None,
)

display(naive_pnl.evaluate_pnls(pnl_cats=pnls).astype("float").round(2))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/5d25702a0300159afafcef4c37ebf3750d1fc8c462e6afd9637b1ce91cfcd497.png
xcat CTOT_NSA_P1W4WL1_ZN_PZN CTOT_NSA_P1W4WL1_ZN_FA_PZN
Return % 6.18 4.23
St. Dev. % 10.00 10.00
Sharpe Ratio 0.62 0.42
Sortino Ratio 0.93 0.63
Max 21-Day Draw % -24.64 -13.74
Max 6-Month Draw % -28.67 -25.14
Peak to Trough Draw % -29.66 -34.97
Top 5% Monthly PnL Share 1.18 1.65
EUR_FXXR_NSA correl -0.05 -0.07
USD_EQXR_NSA correl 0.05 -0.02
USD_DU05YXR_NSA correl -0.04 -0.05
Traded Months 309.00 309.00

Relative FX strategy #

Specs and panel test #

# Basic specs

dict_fxr = {
    "sigs": [
        "CTOT_NSA_CHG_ZNvGFX",
        "CTOT_NSA_P1W4WL1_ZNvGFX",
        "CTOT_NSA_P1M12ML1_ZNvGFX",
        "CTOT_NSA_P1M60ML1_ZNvGFX",
    ],
    "targ": "FXXR_VT10vGFX",
    "cids": cids_fx,
    "start": "2000-01-01",
    "black": fxblack,
    "crs": None,
    "srr": None,
    "pnls": None,
}
# Panel correlation test

dix = dict_fxr
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

# Store correlation results

crs = [
    msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="m",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )
    for sig in sigx
]

dix["crs"] = crs
# Display tests

dix = dict_fxr
crs = dix['crs']

msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=2,
    nrow=2,
    figsize=(12, 12),
    prob_est="map",
    coef_box="lower left",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/c749819e79604c41d396221b677493af81c2fdeeb35acf5e772709c5fcee8860.png

Accuracy and correlation check #

# Signal-return relations object and results

dix = dict_fxr
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    sigs=sigx,
    cosp=True,
    rets=targ,
    freqs=["M"],
    blacklist=blax,
    slip=1
)

display(srr.signals_table().astype("float").round(3).T)

dix["srr"] = srr
Return FXXR_VT10vGFX
Signal CTOT_NSA_CHG_ZNvGFX CTOT_NSA_P1W4WL1_ZNvGFX CTOT_NSA_P1M12ML1_ZNvGFX CTOT_NSA_P1M60ML1_ZNvGFX
Frequency M M M M
Aggregation last last last last
accuracy 0.506 0.505 0.509 0.505
bal_accuracy 0.507 0.505 0.509 0.507
pos_sigr 0.472 0.498 0.485 0.457
pos_retr 0.515 0.515 0.515 0.515
pos_prec 0.522 0.520 0.524 0.522
neg_prec 0.491 0.491 0.494 0.491
pearson 0.018 0.023 0.016 0.011
pearson_pval 0.127 0.052 0.179 0.359
kendall 0.009 0.015 0.009 0.007
kendall_pval 0.260 0.055 0.256 0.366
auc 0.507 0.505 0.509 0.507
# Accuracy bars

dix = dict_fxr
srr = dix["srr"]
sigs = dix["sigs"]

srr.accuracy_bars(
    type="signals", sigs=sigs, title=None, size=(16, 5), title_fontsize=20
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/4ad574e0c7a8d42121d01d0737dcc84622a96457854ee29b43c7f566ca565fcc.png

Naive PnL #

dix = dict_fxr
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

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

for sig in list(sigx):
    naive_pnl.make_pnl(
        sig,
        sig_add=0,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

naive_pnl.make_long_pnl(label="Long only", vol_scale=10)
dix["pnls"] = naive_pnl
dix = dict_fxr
sigx = dix["sigs"]
cidx = dix["cids"]
start = dix["start"]
naive_pnl = dix["pnls"]

pnls = [sig + "_PZN" for sig in sigx] # + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    title_fontsize=16,
    figsize=(16, 8),
    compounding=False,
    xcat_labels=None,
)

display(naive_pnl.evaluate_pnls(pnl_cats=pnls).astype("float").round(2))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/83260c831f1ae497c391fafef62f378b557517379279e3c39da276333a9cdea5.png
xcat CTOT_NSA_CHG_ZNvGFX_PZN CTOT_NSA_P1W4WL1_ZNvGFX_PZN CTOT_NSA_P1M12ML1_ZNvGFX_PZN CTOT_NSA_P1M60ML1_ZNvGFX_PZN
Return % 2.86 2.97 3.09 2.03
St. Dev. % 10.00 10.00 10.00 10.00
Sharpe Ratio 0.29 0.30 0.31 0.20
Sortino Ratio 0.41 0.44 0.44 0.29
Max 21-Day Draw % -13.32 -11.73 -13.00 -17.48
Max 6-Month Draw % -20.92 -20.50 -23.33 -24.05
Peak to Trough Draw % -49.42 -30.45 -50.94 -35.57
Top 5% Monthly PnL Share 1.76 1.67 1.50 2.25
EUR_FXXR_NSA correl 0.06 -0.02 0.05 0.08
USD_EQXR_NSA correl 0.01 -0.02 0.00 0.04
USD_DU05YXR_NSA correl -0.00 0.01 -0.01 -0.01
Traded Months 310.00 310.00 310.00 310.00

Relative equity strategy #

Specs and panel test #

# Basic specs

dict_req = {
    "sigs": [
        "CTOT_NSA_CHG_ZNvGEQ",
        "CTOT_NSA_CHG_ZN_FAvGEQ",
    ],
    "targ": "EQXR_VT10vGEQ",
    "cids": cids_eq,
    "start": "2000-01-01",
    "black": eq_black,
    "crs": None,
    "srr": None,
    "pnls": None,
}
# Panel correlation test

dix = dict_req
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

# Store correlation results

crs = [
    msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="m",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )
    for sig in sigx
]

dix["crs"] = crs
# Display tests

dix = dict_req
crs = dix['crs']

msv.multiple_reg_scatter(
    cat_rels=crs,
    ncol=2,
    nrow=1,
    figsize=(14, 8),
    prob_est="map",
    coef_box="lower left",
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/cf00733701cf80185c04418df1f7e33fb246b9994f809ebe27b3e99cd3ffd010.png

Accuracy and correlation check #

# Signal-return relations object and results

dix = dict_req
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    sigs=sigx,
    cosp=True,
    rets=targ,
    freqs=["M"],
    blacklist=blax,
    slip=1
)

display(srr.signals_table().sort_index().astype("float").round(3).T)

dix["srr"] = srr
Return EQXR_VT10vGEQ
Signal CTOT_NSA_CHG_ZN_FAvGEQ CTOT_NSA_CHG_ZNvGEQ
Frequency M M
Aggregation last last
accuracy 0.511 0.504
bal_accuracy 0.511 0.504
pos_sigr 0.494 0.476
pos_retr 0.495 0.495
pos_prec 0.506 0.500
neg_prec 0.515 0.508
pearson 0.031 0.007
pearson_pval 0.026 0.629
kendall 0.022 0.006
kendall_pval 0.020 0.490
auc 0.511 0.504
# Accuracy bars

dix = dict_req
srr = dix["srr"]
sigs = dix["sigs"]

srr.accuracy_bars(
    type="signals",
    sigs=sigs,
    title=None,
    size=(16, 5),
    title_fontsize=20,
    x_labels_rotate=90,
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/494a5dede525337d92888efdb8a5ab68ea4bd2b36a8ddb80386ed16fe8807efe.png

Naive PnL #

dix = dict_req
sigx = dix["sigs"]
targ = dix["targ"]
cidx = dix["cids"]
start = dix["start"]
blax = dix["black"]

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

for sig in list(sigx):
    naive_pnl.make_pnl(
        sig,
        sig_add=0,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig + "_PZN",
    )

naive_pnl.make_long_pnl(label="Long only", vol_scale=10)
dix["pnls"] = naive_pnl
dix = dict_req
sigx = dix["sigs"]
cidx = dix["cids"]
start = dix["start"]
naive_pnl = dix["pnls"]

pnls = [sig + "_PZN" for sig in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    title_fontsize=16,
    figsize=(16, 8),
    compounding=False,
    xcat_labels=None,
)

display(naive_pnl.evaluate_pnls(pnl_cats=pnls).astype("float").round(2))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/dbeb3f4e6cdad8c036c7ce1fa5ff11305b6b623c5af249ba3a209b0f42db70cc.png
xcat CTOT_NSA_CHG_ZNvGEQ_PZN CTOT_NSA_CHG_ZN_FAvGEQ_PZN
Return % 0.72 3.54
St. Dev. % 10.00 10.00
Sharpe Ratio 0.07 0.35
Sortino Ratio 0.10 0.51
Max 21-Day Draw % -13.57 -13.96
Max 6-Month Draw % -15.78 -21.77
Peak to Trough Draw % -34.75 -29.88
Top 5% Monthly PnL Share 4.81 0.95
EUR_FXXR_NSA correl -0.01 -0.00
USD_EQXR_NSA correl 0.06 0.00
USD_DU05YXR_NSA correl 0.01 0.00
Traded Months 310.00 310.00

Strategy portfolio PnL #

Single factor strategies #

# Dictionary for all single-factor strategies

all_naive_pnls = {}
# Collect all single-signal strategy parameters

dict_pnls = {
    "DFX": {
        "title": "Directional FX",
        "dict": dict_fxd,
        "sig": "CTOT_NSA_CHG_ZN",
        "diversity": 2,
    },
    "RFX": {
        "title": "Relative FX",
        "dict": dict_fxr,
        "sig": "CTOT_NSA_CHG_ZNvGFX",
        "diversity": 2,
    },
    "DEQ": {
        "title": "Directional equity",
        "dict": dict_deq,
        "sig": "CTOT_NSA_CHG_ZN_FA",
        "diversity": 1,
    },
    "REQ": {
        "title": "Relative equity",
        "dict": dict_req,
        "sig": "CTOT_NSA_CHG_ZN_FAvGEQ",
        "diversity": 2,
    },
    "GED": {
        "title": "Equity Duration",
        "dict": dict_ged,
        "sig": "CTOT_NSA_P1W4WL1_ZN",
        "diversity": 1,
    },
}

# Signal labels
dict_sig_labs = {
    "CTOT_NSA_CHG_ZN": "ToT change, all horizons, % ar",
    "CTOT_NSA_CHG_ZN_FA": "ToT change adjusted for real appreciation, all horizons, % ar",
    "CTOT_NSA_P1W4WL1_ZN": "ToT 1-week change vs 4-week average, % ar",
    "CTOT_NSA_CHG_ZNvGFX": "Rel. ToT change, all horizons, % ar",
    "CTOT_NSA_CHG_ZN_FAvGEQ": "Rel. ToT change adjusted for appreciation, all horizons, % ar",
}
# Display PnL tables and collect figures

figs = []
axs = []
tabs = []

for k, v in dict_pnls.items():

    title = v["title"]
    dix = v["dict"]
    sig = v["sig"]
    dp = v["diversity"]

    pnl_name = f"{sig}_{k}"
    pnl_lab ={pnl_name: dict_sig_labs[sig]}

    targ = dix["targ"]
    cidx = dix["cids"]
    start = dix["start"]
    black = dix["black"]

    naive_pnl = msn.NaivePnL(
        dfx,
        ret=targ,
        sigs=[sig],
        cids=cidx,
        start=start,
        blacklist=black,
        bms=["EUR_FXXR_NSA", "USD_EQXR_NSA", "USD_DU05YXR_NSA"],
    )

    naive_pnl.make_pnl(
        sig,
        sig_op="zn_score_pan",
        thresh=2,
        rebal_freq="monthly",
        neutral="zero",
        vol_scale=None,
        leverage=(1 / len(cidx))**(1/dp),
        rebal_slip=1,
        pnl_name=pnl_name,
    )

    figs.append(plt.figure())
    axs.append(
        naive_pnl.plot_pnls(
            pnl_cats=[pnl_name],
            pnl_cids=["ALL"],
            start=start,
            figsize=(16, 8),
            compounding=False,
            title=title,
            return_fig=True,
            xcat_labels=pnl_lab
        )
    )

    tabs.append(naive_pnl.evaluate_pnls(pnl_cats=[pnl_name]))
    all_naive_pnls[pnl_name] = naive_pnl

for fig in figs:
    plt.close(fig)
# Multi-Pnl plot figure 

def plot_axes(axs, rows, cols, figsize=(16, 6)):
    fig, new_axs = plt.subplots(rows, cols, figsize=figsize)
    if isinstance(new_axs, np.ndarray):
        new_axs = new_axs.flatten()
    else:
        new_axs = [new_axs]
    for i, ax in enumerate(axs):
        for line in ax.get_lines():
            new_axs[i].plot(line.get_xdata(), line.get_ydata(),
                        color=line.get_color(),
                        linewidth=line.get_linewidth(),
                        linestyle=line.get_linestyle(),
                        label=line.get_label())

        new_axs[i].set_xlim(ax.get_xlim())
        new_axs[i].set_ylim(ax.get_ylim())
        new_axs[i].set_title(ax.get_title())
        new_axs[i].set_xlabel(ax.get_xlabel())
        new_axs[i].set_ylabel(ax.get_ylabel())
        # Copy x-axis formatter and locator
        new_axs[i].xaxis.set_major_formatter(ax.xaxis.get_major_formatter())
        new_axs[i].xaxis.set_major_locator(ax.xaxis.get_major_locator())
        legend = ax.get_legend()
        if legend:
            labels = [t.get_text() for t in legend.get_texts()]
            handles = [h for h in legend.legend_handles]
            new_axs[i].legend(handles, labels)
    for j in range(len(axs), len(new_axs)):
        new_axs[j].remove()
    return fig, new_axs
# Plot all PnLs

new_fig, new_axs = plot_axes(axs, 3, 2, figsize=(20, 15))
new_fig.suptitle("Single-signal strategy PnLs", fontsize=22, y=0.94)
plt.show()
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/805194edfb98344753be93209bbdfa4d36e8afc67cdd0830547e85f766474ea3.png
# Summary table

sumtab= pd.concat(tabs, axis=1).astype("float").round(2)
sumtab.columns = [lab[-3:] for lab in sumtab.columns]
display(sumtab)
DFX RFX DEQ REQ GED
Return % 5.62 3.75 2.39 3.52 4.53
St. Dev. % 14.40 13.13 8.00 9.95 7.33
Sharpe Ratio 0.39 0.29 0.30 0.35 0.62
Sortino Ratio 0.57 0.41 0.43 0.51 0.93
Max 21-Day Draw % -23.24 -17.49 -21.34 -13.90 -18.06
Max 6-Month Draw % -35.80 -27.46 -22.64 -21.67 -21.01
Peak to Trough Draw % -54.83 -64.88 -40.29 -29.74 -21.74
Top 5% Monthly PnL Share 1.34 1.76 1.54 0.95 1.18
EUR_FXXR_NSA correl 0.02 0.06 0.02 -0.00 -0.05
USD_EQXR_NSA correl -0.01 0.01 0.13 0.00 0.05
USD_DU05YXR_NSA correl -0.00 -0.00 -0.03 0.00 -0.04
Traded Months 310.00 310.00 310.00 310.00 309.00

Combined PnL #

# Combine PnL data into a single quantamental dataframe

unit_pnl_name = "PNLx"

df_pnls = QuantamentalDataFrame.from_qdf_list(
    [
        QuantamentalDataFrame.from_long_df(
            df=pnl.df[
                (pnl.df["xcat"] == key) & (pnl.df["cid"] == "ALL")
            ],
            cid=key[-3:],
            xcat=unit_pnl_name,
        )
        for key, pnl in all_naive_pnls.items()
    ]
)
# Correlation matrix

cids_pnls = [key[-3:] for key in all_naive_pnls.keys()]
cids_labels ={
    "DFX": "Directional FX",
    "DEQ": "Direct. Equity",
    "GED": "Equity Duration",
    "RFX": "Relative FX",
    "REQ": "Rel. Equity",
}

msp.correl_matrix(
    df_pnls,
    xcats=["PNLx"],
    cids=cids_pnls,
    freq="M",
    start="2000-01-01",
    title="Strategies based on single terms of trade signals: Correlation of monthly PnLs",
    title_fontsize=20,
    size=(16, 8),
    cluster=True,
    cid_labels=cids_labels
)
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/0ea58ac1efdbace956157225ad8f5182a823337c2fd61a7524ca81102673ad4e.png
# Create a wide dataframe of equal signals for each strategy

sig_value = 1
usig = "USIG"  # unit signal name
pnl_names=[pnl[-3:] for pnl in all_naive_pnls.keys()]

dt_range = pd.bdate_range(start=dfx["real_date"].min(), end=dfx["real_date"].max())
df_usigs = pd.DataFrame(
    data=sig_value, columns=pnl_names, index=dt_range
)
df_usigs.index.name = "real_date"
df_usigs.columns += f"_{usig}"
# Concat the unit signals to the PNL dataframe

df_usigs = msm.utils.ticker_df_to_qdf(df_usigs)
df_pnls = msm.update_df(df_pnls, df_usigs)
dfx = msm.update_df(dfx, df_pnls)
# Calculate the combined PnL

all_pnl = msn.NaivePnL(
    dfx,
    ret=unit_pnl_name, 
    sigs=[usig],
    cids=pnl_names,
    start="2000-01-01",
    bms=["EUR_FXXR_NSA", "USD_EQXR_NSA", "USD_DU05YXR_NSA"],
)
all_pnl.make_pnl(sig=usig, sig_op="raw", leverage=1/4)

all_pnl.plot_pnls(
    pnl_cats=[f"PNL_{usig}"],
    start="2000-01-01",
    title="Combined naive PnLs of single terms of trade-signal strategies",
    title_fontsize=16,
    figsize=(13, 7),
    xcat_labels=["equally-weighted portfolio of five single-signal strategies"],
)
display(all_pnl.evaluate_pnls(pnl_cats=[f"PNL_{usig}"]))
https://macrosynergy.com/notebooks.build/trading-factors/terms-of-trade-as-trading-signal/_images/f97314e9efc06948214c2e889da439b456f1c5230ad2e962e59c093ca203d81b.png
xcat PNL_USIG
Return % 4.952114
St. Dev. % 8.307024
Sharpe Ratio 0.596136
Sortino Ratio 0.877259
Max 21-Day Draw % -10.857579
Max 6-Month Draw % -17.309781
Peak to Trough Draw % -28.285339
Top 5% Monthly PnL Share 0.908097
EUR_FXXR_NSA correl 0.026601
USD_EQXR_NSA correl 0.043115
USD_DU05YXR_NSA correl -0.016627
Traded Months 310