Cross-country equity futures strategy #

This notebook illustrates the concepts discussed in the post “Cross-country Equity Futures Strategies” available on the Macrosynergy website.

The notebook evaluates five simple thematic and potentially differentiating macro scores across a panel of 16 developed and emerging markets. It demonstrates that even a basic, non-optimized composite score could have added substantial value beyond a risk-parity exposure to global equity index futures. Additionally, a purely relative value equity index futures strategy would have provided respectable long-term returns, complementing passive equity exposure.

The notebook explores both directional and relative equity futures strategies, including:

  • Global Directional Futures Strategy,

  • Directional Futures Strategy for Developed Markets,

  • Global Relative Futures Strategy, and

  • Relative Futures Strategy for Developed Markets

Get packages and JPMaQS data #

# >>> Define constants <<< #
import os

# Minimum Macrosynergy package version required for this notebook
MIN_REQUIRED_VERSION = "0.1.23dev"

# DataQuery credentials: Remember to replace with your own client ID and secret
DQ_CLIENT_ID = os.getenv("DQ_CLIENT_ID")
DQ_CLIENT_SECRET = os.getenv("DQ_CLIENT_SECRET")

# Define any Proxy settings required (http/https)
PROXY = {}

# Start date for the data (argument passed to the JPMaQSDownloader class)
START_DATE = "2000-01-01"
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import warnings
import os
from datetime import date

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

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

# Cross-sections of interest

cids_dmeq = ["AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "SEK", "USD"]
cids_eueq = ["DEM", "ESP", "FRF", "ITL", "NLG"]
cids_aseq = ["CNY", "HKD", "INR", "KRW", "MYR", "SGD", "THB", "TWD"]
cids_exeq = ["BRL", "TRY", "ZAR"]
cids_nueq = ["MXN", "PLN"]

cids_eq = sorted(cids_dmeq + cids_eueq + cids_aseq + cids_exeq + cids_nueq)
cids_eqxe = sorted(list(set(cids_eq) - set(cids_eueq)))
cids_eqxx = sorted(list(set(cids_eqxe) - set(["CNY", "TRY", "PLN", "HKD", "SGD"])))
cids_eqxu = sorted(list(set(cids_eqxx) - set(["USD"])))
cids = cids_eq
# Category tickers

inf = [
    "CPIH_SA_P1M1ML12",
    "CPIH_SJA_P6M6ML6AR",
    "CPIC_SA_P1M1ML12",
    "CPIC_SJA_P6M6ML6AR",
    "INFTEFF_NSA",
]
lab = [
    "EMPL_NSA_P1M1ML12_3MMA",
    "EMPL_NSA_P1Q1QL4",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12",
    "UNEMPLRATE_NSA_D1Q1QL4",
    "WFORCE_NSA_P1Y1YL1_5YMA",
    "WFORCE_NSA_P1Q1QL4_20QMM",
]
ofx = [
    "REEROADJ_NSA_P1M1ML12",
    "NEEROADJ_NSA_P1M1ML12",
    "REER_NSA_P1M1ML12",
]
fin = [
    "RIR_NSA", 
    "FXCRR_NSA", 
    "EQCRR_NSA", 
]
tot = [
    "CTOT_NSA_P1M1ML12",
    "MTOT_NSA_P1M1ML12",
]
add = [
    "RGDP_SA_P1Q1QL4_20QMM",
]

eco = inf + lab + ofx + fin + tot + add

mkt = [
    "EQXR_NSA",
    "EQXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
]

xcats = eco + mkt

# Resultant indicator tickers

tickers = [cid + "_" + xcat for cid in cids_eq for xcat in xcats]
print(f"Maximum number of tickers is {len(tickers)}")
Maximum number of tickers is 624

The description of each JPMaQS category is available either under Macro Quantamental Academy , or on JPMorgan Markets (password protected). In particular, the set used for this notebook is using Consumer price inflation trends , Inflation targets , Labor market dynamics , Demographic trends , Openness-adjusted effective appreciation , Equity index future carry , FX forward carry , Real interest rates , GDP growth , Terms-of-trade , Equity index future returns , and FX tradeability and flexibility

# Download from DataQuery
with JPMaQSDownload(
    client_id=DQ_CLIENT_ID, client_secret=DQ_CLIENT_SECRET
) as downloader:
    df: pd.DataFrame = downloader.download(
        tickers=tickers,
        start_date=START_DATE,
        metrics=["value"],
        suppress_warning=True,
        show_progress=True,
        report_time_taken=True,
        proxy=PROXY,
    )
Downloading data from JPMaQS.
Timestamp UTC:  2024-08-16 13:19:06
Connection successful!
Requesting data: 100%|██████████| 32/32 [00:06<00:00,  4.88it/s]
Downloading data: 100%|██████████| 32/32 [00:15<00:00,  2.09it/s]
Time taken to download data: 	24.12 seconds.
Some expressions are missing from the downloaded data. Check logger output for complete list.
161 out of 624 expressions are missing. To download the catalogue of all available expressions and filter the unavailable expressions, set `get_catalogue=True` in the call to `JPMaQSDownload.download()`.
Some dates are missing from the downloaded data. 
2 out of 6427 dates are missing.
dfx = df.copy()
dfx.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2839811 entries, 0 to 2839810
Data columns (total 4 columns):
 #   Column     Dtype         
---  ------     -----         
 0   real_date  datetime64[ns]
 1   cid        object        
 2   xcat       object        
 3   value      float64       
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 86.7+ MB

Availability and renaming #

The check_availability() function in macrosynergy.management displays the start dates from which each category is available for each requested country, as well as missing dates or unavailable series.

xcatx = inf
msm.check_availability(df=df, xcats=xcatx, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/c592b494067bb4a483cd582258c744fdf75a4947475d4ec5d24ad77ed0418f70.png
xcatx = lab
msm.check_availability(df=df, xcats=xcatx, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/8f8f215590f60b345ccaf2803e10bed1c2745a92947c2e9e02fb0a2f2bb68ff4.png
xcatx = ofx + tot
msm.check_availability(df=df, xcats=xcatx, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/3cd9a943255bb740d4ca2fcb5dcfa0020c0dc00227636b855554bdd2c19549a4.png
xcatx = fin
msm.check_availability(df=df, xcats=xcatx, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/196bbd72a25a4caffd8b945c9ebd9209103f255d81f5196b2dee3874aeb4cc23.png
xcatx = mkt
msm.check_availability(df=df, xcats=xcatx, cids=cids, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/9b2c251237afaa30dfb79403a356e97df570f8d4fe7d6a950fc0ceba7cbbb125.png

Renaming #

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

# Unify quarterly and 3-month moving average names

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)
# Display availability of re-named and unified indicators

xcatx = eco
msm.check_availability(df=dfx, xcats=xcatx, cids=cids_eqxx, missing_recent=False)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/5fe4f9f97b08afcef6eab34b453731b3958475bb6dc933a10beaf1123f38e044.png

Blacklisting #

Before running the analysis, we use make_blacklist() helper function from macrosynergy package, which creates a standardized dictionary of blacklist periods, i.e. periods that affect the validity of an indicator, based on standardized panels of binary categories.

Put simply, this function allows converting category variables into blacklist dictionaries that can then be passed to other functions. Below, we picked two indicators for FX tradability and flexibility. FXTARGETED_NSA is an exchange rate target dummy, which takes a value of 1 if the exchange rate is targeted through a peg or any regime that significantly reduces exchange rate flexibility and 0 otherwise. FXUNTRADABLE_NSA is also a dummy variable that takes the value one if liquidity in the main FX forward market is limited or there is a distortion between tradable offshore and untradable onshore contracts.

# Create blacklisting dictionary

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')),
 'CNY': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-08-14 00:00:00')),
 'HKD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-08-14 00:00:00')),
 'INR': (Timestamp('2000-01-03 00:00:00'), Timestamp('2004-12-31 00:00:00')),
 'MYR_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2007-11-30 00:00:00')),
 'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2024-08-14 00:00:00')),
 'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2024-08-14 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 checks #

Thematic feature groups #

Inflation shortfall #

Most quantamental indicators that professional investment managers would deploy require careful customization and transformations of original JPMaQS data. Since all quantamental data are standardized information states over a range of countries, transformations are simple.

The panel_calculator() function in macrosynergy.panel makes it easy and intuitive to apply a wide range of transformations to each cross section of a panel by using a string. Below we calculate plausible metrics of excess inflation versus a country’s effective inflation target. update_df() function adds the new indicators to the original dataframe dfx .

# Calculate negative excess inflation rates

# Preparation: for relative target deviations, we need denominator bases that should never be less than 2
infs = [i for i in inf if i[:5] in ["CPIH_", "CPIC_"]]
xcatx = infs
cidx =cids_eqxx

# calculate negative excess inflation rates
calcs = []
calcs += ["INFTEBASIS = INFTEFF_NSA.clip(lower=2) "]
for xc in xcatx:
    calcs += [f"X{xc}_NEG = - {xc} + INFTEFF_NSA "]
    calcs += [f"X{xc}_NEGRAT = X{xc}_NEG / INFTEBASIS "]

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

# Collect in factor dictionary
dict_factors = {}  # Create dictionary to store factors

xinfs = list(dfa['xcat'].unique())
xinf_negs = [x for x in xinfs if x.endswith("_NEGRAT")]
dict_factors["XINF_NEG"] = xinf_negs

The macrosynergy package provides two useful functions, view_ranges() and view_timelines() , which assist in plotting means, standard deviations, and time series of the chosen indicators.

cidx = cids_eqxx
xcatx = [x for x in xinfs if x.startswith("XCPIC_SA_P1M1ML12")]

msp.view_ranges(
    dfx,
    xcats=[xcatx[0]],
    kind="bar",
    title=f"Means and standard deviations of {xcatx[0]}",
    ylab="% annualized",
    start="2000-01-01",
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/bdc1d58471c65572ae114879e07e3cc4c7ac5b9a7db6e1bc6aef3ea3182314a0.png
cidx = cids_eqxx
xcatx = xinf_negs

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/3aaf05e3f7cb75568c7d2a0d1eb1feed53e3619899359c51c26fd4343a816410.png

Labor market slackening #

To understand how the business cycle affects employment growth, we measure the deviation of current employment growth from the long-term workforce median, known as “excess employment growth.” This isolates the portion of growth due to the business cycle, revealing deviations from the long-term trend. The following cell calculates the negative of “excess employment growth” to analyze the impact of negative deviations.

# Calculate indicators of labor market tightening or tightness

cidx = cids_eqxx

calcs = [
    "XEMPL_NSA_P1M1ML12_3MMA_NEG = - EMPL_NSA_P1M1ML12_3MMA + WFORCE_NSA_P1Y1YL1_5YMA",
]

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

# Collect in factor dictionary

lab_slacks = [
    "XEMPL_NSA_P1M1ML12_3MMA_NEG",
    "UNEMPLRATE_NSA_3MMA_D1M1ML12",
]
dict_factors["LAB_SLACK"] = lab_slacks
cidx = cids_eqxx
xcatx = lab_slacks

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/a87d2878cdc64aab31c6233a0b298b7f7563c3f3c0310fc575bb678777a838ca.png

Effective currency depreciation #

# Calculate the negatives of openness-adjusted effective real appreciation collected previously in the list ofx

xcatx = ofx
cidx = cids_eqxx

calcs = []
for xc in xcatx:
    calc = [f"X{xc}_NEG = - {xc}"]
    calcs.extend(calc)

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

# Collect in factor dictionary

fx_deprecs = list(dfa['xcat'].unique())
dict_factors["FX_DEPREC"] = fx_deprecs
cidx = cids_eqxx
xcatx = fx_deprecs

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/08c54eca9dcfb470f225562803bf838b43f842b856fb8f9ca66835fdca60c4c6.png

Ease of local finance #

The idea behind this theme is simply that accommodative local financial conditions support demand for equity. The theme is represented by two indicators:

  • The first is the negative of an excess real short-term interest rate, which is calculated as the difference between a real expectations-based short-term interest rate (view documentation) and estimated productivity growth, whereby the latter is approximated by the difference between a GDP growth trend and work force growth trend .

  • The second indicators excess real equity carry based on the difference between (i) average of expected forward dividend and earnings yield and (ii) the main local-currency real short-term interest rate, in % annualized of notional of the contract (view documentation). Excess here means above 3.5%, a judgment call assuming that for equity carry to be attractive it needs to excess at least 20% of long-term average index future volatility.

# Calculate ease-of-financing indicators
cidx = cids_eqxx

calcs = [
    "XRIR_NSA_NEG = - RIR_NSA + RGDP_SA_P1Q1QL4_20QMM - WFORCE_NSA_P1Y1YL1_5YMA",
    "XEQCRR_NSA = EQCRR_NSA - 0.2 * 17.5"  # excess carry assuming equity vol of 17.5% and 0.2 minimum Sharpe
]

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

# Collect in factor dictionary

ease_fins = list(dfa['xcat'].unique())
dict_factors["EASE_FIN"] = ease_fins
cidx = cids_eqxx
xcatx = ease_fins

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/28d13a810a871baf9225f46141d583d27157040cd31f7bb594b129e4fc0b3897.png

Terms-of-trade improvement #

The indicators for terms of trade improvement are directly accessible on JPMaQS and do not need any adjustments.

# Use annual terms-of-trade growth
tot_poya = ["CTOT_NSA_P1M1ML12", "MTOT_NSA_P1M1ML12"]

dict_factors["TOT_POYA"] = tot_poya
cidx = cids_eqxx
xcatx = tot_poya

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/4a5717df80cc702531f5a0d162a1c59168621ab5d1c711553e7376c75c74d67b.png

Composite features #

Thematic scores #

The make_zn_scores() function is a method for normalizing values across different categories. This is particularly important when summing or averaging categories with different units and time series properties. The function computes z-scores for a category panel around a specified neutral level that may be different from the mean. The term “zn-score” refers to the normalized distance from the neutral value.

The default mode of the function calculates scores based on sequential estimates of means and standard deviations, using only past information. This is controlled by the sequential=True argument, and the minimum number of observations required for meaningful estimates is set with the min_obs argument. By default, the function calculates zn-scores for the initial sample period defined by min_obs on an in-sample basis to avoid losing history.

The means and standard deviations are re-estimated daily by default, but the frequency of re-estimation can be controlled with the est_freq argument, which can be set to weekly, monthly, or quarterly.

# Normalize thematic group members

cidx = cids_eqxx

xcatx = []
for key, value in dict_factors.items():
    xcatx.extend(value)  # list of all categories


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 * 5,
        neutral="zero",
        pan_weight=1,  # assumes comparable impact scales across cross-sections
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)
cidx = cids_eqxx
xcatx = [x + "_ZN" for x  in lab_slacks]  # dict_factors.keys()
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/0bba91433d5b3fb3b6540e12f84eab9bf0e3ba01c2c8c9d7cdae86c7f2875c37.png

linear_composite function is designed to calculate linear combinations of different categories. It can produce a composite even if some of the component data are missing. This flexibility is valuable because it enables to work with the available information rather than discarding it entirely. This behavior is desirable if one works with a composite of a set of categories that capture a similar underlying factor. In this context, the function calculates simple averages of the factors listed in dict_factors .

# Combine to thematic composite thematic scores

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

for key, value in dict_factors.items():
    xcatx = [x + "_ZN" for x in value]
    dfaa = msp.linear_composite(
        df=dfx,
        xcats=xcatx,
        cids=cidx,
        complete_xcats=False,
        new_xcat=key,
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)
cidx = cids_eqxx
xcatx = list(dict_factors.keys())

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/762daaef207fe906064471b23e839675a69c5718f901181a851d63e737690ca1.png
# Re-score composites

cidx = cids_eqxx
xcatx = list(dict_factors.keys())

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 * 5,
        neutral="zero",
        pan_weight=1,  # assumes comparable impact scales across cross-sections
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)
themz = [x + "_ZN" for x in list(dict_factors.keys())]

The below code defines a dictionary dict_themes with various macroeconomic themes and their descriptions, and then creates a list of the keys from this dictionary. This approach ensures that the labels associated with each theme are more readable and accessible.

# Labelling dictionary

dict_themes = {}
dict_themes["ALL_MACRO_ZN"] = "All macro themes"
dict_themes["XINF_NEG_ZN"] = "Inflation shortfall"
dict_themes["LAB_SLACK_ZN"] = "Labor market slackening"
dict_themes["FX_DEPREC_ZN"] = "Effective currency depreciation"
dict_themes["EASE_FIN_ZN"] = "Ease of local finance"
dict_themes["TOT_POYA_ZN"] = "Terms of trade improvement"

macroz = list(dict_themes.keys())
cidx = cids_eqxx
xcatx = themz[:3]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    title="Thematic macro scores for equity index futures, part 1",
    title_fontsize=30,
    xcat_labels=[dict_themes[xc] for xc in xcatx],
    legend_fontsize=20,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)

xcatx = themz[3:]
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    title="Thematic macro scores for equity index futures, part 2",
    title_fontsize=30,
    xcat_labels=[dict_themes[xc] for xc in xcatx],
    legend_fontsize=20,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/3c07fd298957ba2878f5477abc19bca30c819d412b563796204d2f252b75de21.png https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/0e1edabf034c6a9fb0de2dcab0631b408073ab1f7988e7de080f0225372dedfc.png

Grand total macro score #

All five themes—Inflation shortfall, Labor market slackening, Effective currency depreciation, Ease of local finance, and Terms of trade improvement—are combined into a composite score using equal weights. This new indicator is standardized into a z-score and given the suffix _ZN , resulting in the label ALL_MACRO_ZN .There is no optimization involved.

# Combine to grand total

cidx = cids_eqxx
xcatx = themz

dfa = msp.linear_composite(
    df=dfx,
    xcats=xcatx,
    cids=cidx,
    complete_xcats=False,
    new_xcat="ALL_MACRO",
)
dfx = msm.update_df(dfx, dfa)

# Re-score

dfa = msp.make_zn_scores(
    dfx,
    xcat="ALL_MACRO",
    cids=cidx,
    sequential=True,
    min_obs=261 * 5,
    neutral="zero",
    pan_weight=1,  # assumes comparable impact scales across cross-sections
    thresh=3,
    postfix="_ZN",
    est_freq="m",
)
dfx = msm.update_df(dfx, dfa)

# Add to labelling dictionary

dict_themes["ALL_MACRO_ZN"] = "All macro themes" # updating dict_themes with the new indicator 
cidx = cids_eqxx
xcatx = ["ALL_MACRO", "ALL_MACRO_ZN"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/625e5636c33f2405213e11a2b0c1aa118cc6e7136fae009d010c811b698bad56.png
cidx = cids_eqxx
xcatx = ["ALL_MACRO_ZN"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    title="Total macro scores for equity trading signals, based on conceptual parity",
    title_fontsize=30,
    xcat_labels=[dict_themes[xc] for xc in xcatx],
    legend_fontsize=20,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/13f9f55da5546b31688b7bf851e30d7a71dbc31ab33dbbf363b2a787bebc48e6.png

Relative scores #

DM basket #

The convenience function make_relative_value() of the macrosynergy.panel module calculates values relative to an equally-weighted basket of developed market currencies, which are specified in the list cids_dmeq , while adapting to missing periods of any of the basket cross sections. update_df() function in the macrosynergy management module concatenates two JPMaQS data frames, effectively adding the newly calculated relative indicators with postfix vDM .

cidx = cids_dmeq
xcatx = macroz

dfa = msp.make_relative_value(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,  # cross-sections can be blacklisted for calculation and basket use
    rel_meth="subtract",
    complete_cross=False,  # cross-sections do not have to be complete for basket calculation
    postfix="vDM",
)
dfx = msm.update_df(dfx, dfa)

macroz_vdm = [x + "vDM" for x in macroz]
macro = macroz[0]  # check composite score
cidx = cids_dmeq
xcatx = [macro, f"{macro}vDM"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/969826be4732e1c4d5beceeffd8ea9e62e945f4a3047fa7ec4b19e8569a3db25.png

Global basket #

The make_relative_value() function from the macrosynergy.panel module calculates values relative to an equally-weighted basket of available market cids_eqxx . This basket includes all available market cross-sections and adapts to any missing periods within these cross-sections. The newly generated global relative indicators are then appended with the postfix vGM , differentiating them from the relative indicator calculated earlier.

cidx = cids_eqxx
xcatx = macroz

dfa = msp.make_relative_value(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,  # cross-sections can be blacklisted for calculation and basket use
    rel_meth="subtract",
    complete_cross=False,  # cross-sections do not have to be complete for basket calculation
    postfix="vGM",
)
dfx = msm.update_df(dfx, dfa)

macroz_vgm = [x + "vGM" for x in macroz]
The category, LAB_SLACK_ZN, is missing ['INR'] from the requested basket. The new basket will be ['AUD', 'BRL', 'CAD', 'CHF', 'EUR', 'GBP', 'JPY', 'KRW', 'MXN', 'MYR', 'SEK', 'THB', 'TWD', 'USD', 'ZAR'].
macro = macroz[0]  # pick a composite score
cidx = cids_eqxx
xcatx = [macro, f"{macro}vGM"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    title="Total macro scores for equity trading signals, directional and relative to global basket",
    title_fontsize=30,
    xcat_labels=["Directional", "Relative to global average"],
    legend_fontsize=20,
    ncol=4,
    start="2000-01-01",
    same_y=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/ede5a5712920d116884917812452feccc99ba000bbff7f04ad9b42aa18b7d718.png

Target returns #

Outright equity index returns #

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

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
    cumsum=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/cb40a9f37280473094f155f4914ac3fe54f75ab5ddf516f8e8d41e92fd498e2d.png

Relative equity index returns #

DM basket #

The code below calculates relative values for equity index futures relative to a basket of all available market currencies specified in the list cids_dmeq . The newly calculated relative indicators are assigned the suffix vDM .

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

dfa = msp.make_relative_value(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,  # cross-sections can be blacklisted for calculation and basket use
    rel_meth="subtract",
    complete_cross=False,  # cross-sections do not have to be complete for basket calculation
    postfix="vDM",
)
dfx = msm.update_df(dfx, dfa)
cidx = cids_dmeq
xcatx = ["EQXR_NSAvDM", "EQXR_VT10vDM"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
    cumsum=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/df8ff17b21067eb6aa243c14a81ee29b4ae6c0403d4c6680bcd5f14d52f23325.png

Global basket #

The code below calculates relative values for equity index futures relative to a basket of all available market currencies specified in the list cids_eqxx . The newly calculated relative indicators are assigned the suffix vGM .

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

dfa = msp.make_relative_value(
    dfx,
    xcats=xcatx,
    cids=cidx,
    start="2000-01-01",
    blacklist=fxblack,  # cross-sections can be blacklisted for calculation and basket use
    rel_meth="subtract",
    complete_cross=False,  # cross-sections do not have to be complete for basket calculation
    postfix="vGM",
)
dfx = msm.update_df(dfx, dfa)
cidx = cids_eqxx
xcatx = ["EQXR_NSAvGM", "EQXR_VT10vGM"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    start="2000-01-01",
    same_y=True,
    cumsum=True,
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/204979fbb762d7c63cfe60bd5388c504f22c5aa9c43d8d911afc13eff588cc78.png

Value checks #

Global directional futures strategy #

Specs and panel test #

We start by defining a dictionary with the key parameters necessary for evaluating a specific type of strategy. This includes setting the signals, target returns, cross-sections, initial date, and blacklistings for evaluating a global directional futures strategy.

dict_deq_gm = {
    "sigs": macroz,
    "targs": ["EQXR_NSA", "EQXR_VT10"],
    "cidx": cids_eqxx,
    "start": "2000-01-01",
    "black": fxblack,
    "srr": None,
    "pnls": None,
}

Instances of the CategoryRelations class from the macrosynnergy.panel package are designed to organize panels of features and targets into formats suitable for analysis. This class provides functionalities for frequency conversion, adding lags, and trimming outliers.

dix = dict_deq_gm

sig = dix["sigs"][0]
targ = dix["targs"][1]
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

cr = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start=start,
    blacklist=blax,
)
cr.reg_scatter(
    labels=False,
    coef_box="lower left",
    title="Total macro score and subsequent equity index futures returns, 16 DM/EM markets, since 2000",
    xlab="Total (all themes) macro score, end-of-quarter",
    ylab="Equity index futures return for 10% ar vol target, %, next quarter",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/3ca11d90b4f6947d077834873d19088083dc588cb9246926200f8ad0c7b62d09.png

multiple_reg_scatter() method allows comparison of several pairs of two categories relationships side by side, including the strength of the linear association and any potential outliers.

dix = dict_deq_gm

sigs = dix["sigs"]
targ = dix["targs"][1] # assuming just one target
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

# Initialize the dictionary to store CategoryRelations instances

dict_cr = {}

for sig in sigs:
    dict_cr[sig] = msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )

# Plotting the results

crs = list(dict_cr.values())
crs_keys = list(dict_cr.keys())

msv.multiple_reg_scatter(
    cat_rels=crs,
    title="Macro scores and subsequent equity index futures returns, 16 DM/EM markets, since 2000",
    ylab="Equity index futures return for 10% ar vol target, %, next quarter",
    ncol=3,
    nrow=2,
    figsize=(15, 10),
    prob_est="map",
    coef_box="lower left",
    subplot_titles=[dict_themes[k] for k in crs_keys],
)
LAB_SLACK_ZN misses: ['INR'].
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/656a909e2bf33a5abd2d6e4e20d2e6fd13462682b9b6ebdb57773a40e9812a96.png

Accuracy and correlation check #

Please refer to the SignalReturnRelations class of the macrosynergy.signal module for details on the code below.

dix = dict_deq_gm

sigx = dix["sigs"]
targx = dix["targs"]
cidx = dix["cidx"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=sigx,
    rets=targx,
    freqs="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_deq_gm

srr = dix["srr"]

display(srr.multiple_relations_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_NSA ALL_MACRO_ZN M last 0.541 0.534 0.541 0.589 0.620 0.447 0.128 0.000 0.070 0.000 0.534
EASE_FIN_ZN M last 0.564 0.527 0.734 0.592 0.606 0.448 0.044 0.004 0.035 0.001 0.522
FX_DEPREC_ZN M last 0.512 0.514 0.489 0.589 0.603 0.424 0.039 0.011 0.027 0.007 0.514
LAB_SLACK_ZN M last 0.483 0.504 0.390 0.591 0.595 0.412 0.081 0.000 0.038 0.000 0.504
TOT_POYA_ZN M last 0.511 0.516 0.475 0.589 0.606 0.426 0.052 0.001 0.019 0.063 0.516
XINF_NEG_ZN M last 0.537 0.527 0.558 0.590 0.614 0.441 0.086 0.000 0.044 0.000 0.528
EQXR_VT10 ALL_MACRO_ZN M last 0.540 0.533 0.541 0.589 0.620 0.447 0.104 0.000 0.064 0.000 0.534
EASE_FIN_ZN M last 0.564 0.526 0.734 0.592 0.606 0.447 0.031 0.043 0.026 0.011 0.521
FX_DEPREC_ZN M last 0.512 0.514 0.489 0.589 0.603 0.424 0.023 0.128 0.019 0.064 0.514
LAB_SLACK_ZN M last 0.483 0.503 0.390 0.591 0.594 0.412 0.068 0.000 0.032 0.003 0.503
TOT_POYA_ZN M last 0.512 0.516 0.475 0.589 0.606 0.426 0.035 0.023 0.017 0.105 0.517
XINF_NEG_ZN M last 0.537 0.527 0.558 0.590 0.613 0.440 0.081 0.000 0.044 0.000 0.527

Naive PnL #

The NaivePnL class of the macrosynergy.pnl module is the basis for calculating simple stylized PnLs for various signals under consideration of correlation benchmarks.

dix = dict_deq_gm

sigx = dix["sigs"]
targ = dix["targs"][1]
cidx = dix["cidx"]
blax = dix["black"]
start = dix["start"]

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

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

for sig in sigx[:2]:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="raw",
        sig_add=1,
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig+"_RAWLB1"
    )

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

dix["pnls"] = naive_pnl
dix = dict_deq_gm

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sigs"][0]]

naive_pnl = dix["pnls"]
vers = ["_RAW", "_RAWLB1"]
pnls = [s + v for s in sigx for v in vers] + ["Long only"]

desc = [
    "based on total macro score (no long bias)",
    "based on total macro score, with long bias (1 standard deviation)",
    "long only, risk parity",
    
]

labels = {key: desc for key, desc in zip(pnls, desc)}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Global equity index futures portfolio PnLs, scaled to 10% annualized volatility",
    xcat_labels=labels,
    figsize=(16, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/22ec6194d82bf972d121efdd71e9775c077976e266d1b78d23c0c4c7ac4c730a.png
dix = dict_deq_gm

start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][2:]

lab={key + "_RAW": value for key, value in dict_themes.items()}
labels = dict(list(lab.items())[2:])

naive_pnl = dix["pnls"]
pnls = [s + "_RAW" for s in sigx]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    xcat_labels=labels,
    figsize=(18, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/b862431687f12d9593e7757904e42fc616725af1a0b1cdcfd61a245509d58295.png
dix = dict_deq_gm
start = dix["start"]
sigx = dix["sigs"]
naive_pnl = dix["pnls"]

pnls = [sig + "_RAW" for sig in sigx] + ["ALL_MACRO_ZN_RAWLB1", "Long only"]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)

labels={key + "_RAW": value for key, value in dict_themes.items()}
labels['ALL_MACRO_ZN_RAWLB1'] = 'All macro, long bias (1std)'

df_eval = df_eval.rename(columns=labels)

display(df_eval.transpose().astype("float").round(3))
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
All macro themes 8.384 10.0 0.838 1.227 -23.486 -17.913 -28.833 0.665 0.101 296.0
All macro, long bias (1std) 9.136 10.0 0.914 1.274 -32.626 -21.495 -35.408 0.499 0.480 296.0
Ease of local finance 5.450 10.0 0.545 0.759 -23.770 -21.459 -34.511 0.810 0.355 296.0
Effective currency depreciation 2.399 10.0 0.240 0.348 -16.076 -29.160 -56.957 1.772 0.069 296.0
Labor market slackening 4.057 10.0 0.406 0.601 -18.514 -14.848 -31.589 1.355 -0.093 296.0
Long only 6.153 10.0 0.615 0.837 -25.980 -27.678 -35.869 0.549 0.598 296.0
Terms of trade improvement 2.765 10.0 0.277 0.406 -18.380 -30.097 -46.496 1.733 0.038 296.0
Inflation shortfall 6.109 10.0 0.611 0.885 -20.358 -25.713 -37.466 0.902 -0.095 296.0

DM directional futures strategy #

This section deals with the same global directional futures strategy as above, but just for developed markets.

Specs and panel test #

dict_deq_dm = {
    "sigs": macroz,
    "targs": ["EQXR_NSA", "EQXR_VT10"],
    "cidx": cids_dmeq,
    "start": "2000-01-01",
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_deq_dm

sig = dix["sigs"][0]
targ = dix["targs"][1]
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

cr = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start=start,
    blacklist=blax,
)
cr.reg_scatter(
    labels=False,
    coef_box="lower left",
    title="Total macro score and subsequent equity index futures returns, 8 DM markets, since 2000",
    xlab="Total (all themes) macro score, end-of-quarter",
    ylab="Equity index futures return for 10% ar vol target, %, next quarter",
    size=(10, 6),
    prob_est="map",
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/6de598da139427db0222969f4c0bfb5857f9efbe70d1ad2908be0c69386f8222.png
dix = dict_deq_dm

sigs = dix["sigs"]
targ = dix["targs"][1]
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

# Initialize the dictionary to store CategoryRelations instances

dict_cr = {}

for sig in sigs:
    dict_cr[sig] = msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )

# Plotting the results

crs = list(dict_cr.values())
crs_keys = list(dict_cr.keys())

msv.multiple_reg_scatter(
    cat_rels=crs,
    title="Macro scores and subsequent equity index futures returns, 8 DM markets, since 2000",
    ylab="Equity index futures return for 10% ar vol target, %, next quarter",
    ncol=3,
    nrow=2,
    figsize=(15, 10),
    prob_est="map",
    coef_box="upper right",
    subplot_titles=[dict_themes[k] for k in crs_keys],
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/0f5877e35b44a88b1ea524f2f3fd2a7053b9b5d294e3b156df14ea7649b00afa.png

Accuracy and correlation check #

dix = dict_deq_dm

sigx = dix["sigs"]
targx = dix["targs"]
cidx = dix["cidx"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=sigx,
    rets=targx,
    freqs="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_deq_dm

srr = dix["srr"]

display(srr.multiple_relations_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_NSA ALL_MACRO_ZN M last 0.556 0.541 0.578 0.605 0.640 0.442 0.142 0.000 0.084 0.000 0.542
EASE_FIN_ZN M last 0.588 0.535 0.808 0.607 0.621 0.449 0.053 0.012 0.034 0.015 0.523
FX_DEPREC_ZN M last 0.490 0.493 0.484 0.605 0.598 0.388 0.002 0.911 -0.005 0.708 0.493
LAB_SLACK_ZN M last 0.475 0.506 0.355 0.605 0.613 0.399 0.084 0.000 0.047 0.001 0.506
TOT_POYA_ZN M last 0.527 0.531 0.480 0.605 0.637 0.425 0.056 0.008 0.036 0.010 0.532
XINF_NEG_ZN M last 0.562 0.540 0.612 0.606 0.637 0.442 0.099 0.000 0.065 0.000 0.540
EQXR_VT10 ALL_MACRO_ZN M last 0.555 0.540 0.578 0.605 0.639 0.441 0.114 0.000 0.069 0.000 0.541
EASE_FIN_ZN M last 0.587 0.534 0.808 0.607 0.620 0.447 0.026 0.214 0.014 0.331 0.522
FX_DEPREC_ZN M last 0.490 0.493 0.484 0.605 0.598 0.388 -0.008 0.706 -0.006 0.683 0.493
LAB_SLACK_ZN M last 0.475 0.505 0.355 0.605 0.612 0.399 0.073 0.000 0.036 0.010 0.505
TOT_POYA_ZN M last 0.527 0.532 0.480 0.605 0.638 0.426 0.055 0.009 0.034 0.015 0.533
XINF_NEG_ZN M last 0.561 0.539 0.612 0.606 0.636 0.441 0.077 0.000 0.050 0.000 0.539

Naive PnL #

dix = dict_deq_dm

sigx = dix["sigs"]
targ = dix["targs"][1]
cidx = dix["cidx"]
blax = dix["black"]
start = dix["start"]

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

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

for sig in sigx[:2]:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        sig_op="raw",
        sig_add=1, # long bias, 1 standard deviation
        thresh=3,
        rebal_freq="monthly",
        vol_scale=10,
        rebal_slip=1,
        pnl_name=sig+"_RAWLB1"
    )

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

dix["pnls"] = naive_pnl
dix = dict_deq_dm

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sigs"][0]]

naive_pnl = dix["pnls"]
vers = ["_RAW", "_RAWLB1"]
pnls = [s + v for s in sigx for v in vers] + ["Long only"]

desc = [
    "based on total macro score (no long bias)",
    "based on total macro score, with long bias (1 standard deviation)",
    "long only, risk parity",
    
]

labels = {key: desc for key, desc in zip(pnls, desc)}


naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Global equity index futures portfolio PnLs, scaled to 10% annualized volatility",
    xcat_labels=labels,
    figsize=(16, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/d38408330760fd7e8f68b98fd0dc078353473d925130a878ec394608a09a8124.png
dix = dict_deq_dm

start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][2:]

naive_pnl = dix["pnls"]
pnls = [s + "_RAW" for s in sigx]

lab={key + "_RAW": value for key, value in dict_themes.items()}
labels = dict(list(lab.items())[2:])

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    xcat_labels=labels,
    figsize=(18, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/821f59b24b9bea125857135e3c386c38d7a1e02b6d44ff4d1572055830a6e635.png
dix = dict_deq_dm
start = dix["start"]
sigx = dix["sigs"]
naive_pnl = dix["pnls"]

pnls = [sig + "_RAW" for sig in sigx] + ["ALL_MACRO_ZN_RAWLB1", "Long only"]

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)

labels={key + "_RAW": value for key, value in dict_themes.items()}
labels['ALL_MACRO_ZN_RAWLB1'] = 'All macro, long bias (1std)'

df_eval = df_eval.rename(columns=labels)

display(df_eval.transpose().astype("float").round(3))
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
All macro themes 7.721 10.0 0.772 1.125 -23.387 -13.383 -24.231 0.666 0.224 296.0
All macro, long bias (1std) 7.975 10.0 0.798 1.115 -30.212 -18.675 -31.209 0.513 0.547 296.0
Ease of local finance 4.940 10.0 0.494 0.681 -25.641 -20.278 -26.428 0.766 0.515 296.0
Effective currency depreciation -1.004 10.0 -0.100 -0.142 -12.770 -23.528 -74.030 -3.376 0.005 296.0
Labor market slackening 2.625 10.0 0.263 0.391 -17.763 -15.278 -38.370 1.949 -0.080 296.0
Long only 5.482 10.0 0.548 0.748 -24.637 -22.204 -41.303 0.574 0.636 296.0
Terms of trade improvement 5.316 10.0 0.532 0.784 -12.585 -24.376 -31.203 0.769 0.000 296.0
Inflation shortfall 4.842 10.0 0.484 0.693 -18.332 -27.845 -38.098 0.965 -0.003 296.0
dix = dict_deq_dm
cidx = dix["cidx"]
sig = dix["sigs"][0]
start = dix["start"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_RAW",
    pnl_cids=cidx,
    freq="q",
    start=start,
    figsize=(15, 4),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/83ff2e0d76acec943d32f484fbc26e92c3898db94698984246cd5e7145f18152.png

Global relative futures strategy #

This section examines global relative value signals.

Specs and panel test #

dict_req_gm = {
    "sigs": macroz_vgm,
    "targs": ["EQXR_VT10vGM"],
    "cidx": cids_eqxx,
    "start": "2000-01-01",
    "black": fxblack,
    "srr": None,
    "pnls": None,
}

# For labelling
dict_themes_vgm = {
    key + "vGM": value + " (relative)" for key, value in dict_themes.items()
}
dix = dict_req_gm

sigs = dix["sigs"]
targ = dix["targs"][0]
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

# Initialize the dictionary to store CategoryRelations instances

dict_cr = {}

for sig in sigs:
    dict_cr[sig] = msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )

# Plotting the results

crs = list(dict_cr.values())
crs_keys = list(dict_cr.keys())

msv.multiple_reg_scatter(
    cat_rels=crs,
    title="Relative macro scores and subsequent relative equity futures returns, 16 DM/EM markets, since 2000",
    ylab="Equity index futures return for 10% ar vol target, versus global basket, %, next quarter",
    ncol=3,
    nrow=2,
    figsize=(15, 10),
    prob_est="map",
    coef_box="lower left",
    subplot_titles=[dict_themes_vgm[k] for k in crs_keys],
)
LAB_SLACK_ZNvGM misses: ['INR'].
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/c1d952259d6f47efc33653f0fc13ca37fad47e74e79dd5ff412b4b2be9f0616d.png

Accuracy and correlation check #

dix = dict_req_gm

sigx = dix["sigs"]
targx = dix["targs"]
cidx = dix["cidx"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=sigx,
    rets=targx,
    freqs="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_req_gm
srr = dix["srr"]

display(srr.multiple_relations_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_VT10vGM ALL_MACRO_ZNvGM M last 0.515 0.515 0.510 0.498 0.513 0.517 0.062 0.000 0.034 0.001 0.515
EASE_FIN_ZNvGM M last 0.521 0.521 0.551 0.499 0.518 0.524 0.043 0.006 0.030 0.004 0.521
FX_DEPREC_ZNvGM M last 0.520 0.520 0.500 0.498 0.518 0.522 0.053 0.001 0.031 0.002 0.520
LAB_SLACK_ZNvGM M last 0.503 0.503 0.519 0.497 0.501 0.506 0.011 0.509 0.008 0.472 0.503
TOT_POYA_ZNvGM M last 0.513 0.513 0.494 0.498 0.511 0.515 0.020 0.196 0.014 0.162 0.513
XINF_NEG_ZNvGM M last 0.501 0.501 0.486 0.498 0.499 0.502 0.008 0.606 0.000 0.982 0.501

Naive PnL #

dix = dict_req_gm

sigx = dix["sigs"]
targ = dix["targs"][0]
cidx = dix["cidx"]
blax = dix["black"]
start = dix["start"]

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

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


dix["pnls"] = naive_pnl
dix = dict_req_gm

start = dix["start"]
cidx = dix["cidx"]
sig = dix["sigs"][0]

naive_pnl = dix["pnls"]
pnls = [sig + "_RAW"]


naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Global equity index futures relative value PnL, scaled to 10% annualized volatility",
    xcat_labels=["based on total macro score for each country (conceptual parity) relative to the global average"],
    figsize=(16, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/0826523d7a689da03a2ec9d530f36f3d7a37f31cd4f6e40df976f87fad6ecb8d.png
dix = dict_req_gm

start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][1:]

naive_pnl = dix["pnls"]
pnls = [s + "_RAW" for s in sigx] 

lab={key + "_RAW": value for key, value in dict_themes_vgm.items()}
labels = dict(list(lab.items())[1:])

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    xcat_labels=labels,
    figsize=(18, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/9fe8290e5b86165e561e8208498f685d3ec74efac6488737574e7b30d4050ca4.png
dix = dict_req_gm
start = dix["start"]
sigx = dix["sigs"]

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

labels={key + "_RAW": value for key, value in dict_themes_vgm.items()}


df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)
df_eval = df_eval.rename(columns=labels)

display(df_eval.transpose().astype("float").round(3))
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
All macro themes (relative) 5.660 10.0 0.566 0.812 -7.488 -11.833 -17.532 0.662 -0.074 296.0
Ease of local finance (relative) 3.365 10.0 0.336 0.476 -11.905 -12.580 -20.761 0.995 -0.113 296.0
Effective currency depreciation (relative) 5.238 10.0 0.524 0.759 -9.714 -17.207 -23.516 0.609 -0.017 296.0
Labor market slackening (relative) 0.954 10.0 0.095 0.136 -13.063 -22.501 -45.762 3.069 -0.095 296.0
Terms of trade improvement (relative) 1.691 10.0 0.169 0.242 -20.666 -17.326 -31.883 2.164 0.104 296.0
Inflation shortfall (relative) 0.790 10.0 0.079 0.114 -8.679 -16.512 -31.867 4.511 -0.092 296.0

DM relative futures strategy #

Relative equity futures strategy for developed markets

Specs and panel test #

dict_req_dm = {
    "sigs": macroz_vdm,
    "targs": ["EQXR_VT10vDM"],
    "cidx": cids_dmeq,
    "start": "2000-01-01",
    "black": fxblack,
    "srr": None,
    "pnls": None,
}

# For labelling
dict_themes_vdm = {
    key + "vDM": value + " (relative)" for key, value in dict_themes.items()
}
dix = dict_req_dm

sigs = dix["sigs"]
targ = dix["targs"][0]
blax = dix["black"]
cidx = dix["cidx"]
start = dix["start"]

# Initialize the dictionary to store CategoryRelations instances

dict_cr = {}

for sig in sigs:
    dict_cr[sig] = msp.CategoryRelations(
        dfx,
        xcats=[sig, targ],
        cids=cidx,
        freq="Q",
        lag=1,
        xcat_aggs=["last", "sum"],
        start=start,
        blacklist=blax,
    )

# Plotting the results

crs = list(dict_cr.values())
crs_keys = list(dict_cr.keys())

msv.multiple_reg_scatter(
    cat_rels=crs,
    title="Macro scores and subsequent equity index futures returns, 16 DM/EM markets, since 2000",
    ylab="Equity index futures return for 10% ar vol target, versus global basket, %, next quarter",
    ncol=3,
    nrow=2,
    figsize=(15, 10),
    prob_est="map",
    coef_box="lower left",
    subplot_titles=[dict_themes_vdm[k] for k in crs_keys],
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/52440d928f882a2ff39d0dc6304a1b81bc89d3ec75567bf15229d8501d1a390a.png

Accuracy and correlation check #

dix = dict_req_dm

sigx = dix["sigs"]
targx = dix["targs"]
cidx = dix["cidx"]
start = dix["start"]
blax = dix["black"]

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sigs=sigx,
    rets=targx,
    freqs="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_req_dm
srr = dix["srr"]

display(srr.multiple_relations_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_VT10vDM ALL_MACRO_ZNvDM M last 0.513 0.513 0.500 0.497 0.510 0.516 0.044 0.035 0.026 0.064 0.513
EASE_FIN_ZNvDM M last 0.522 0.522 0.501 0.499 0.521 0.524 0.005 0.826 0.010 0.464 0.522
FX_DEPREC_ZNvDM M last 0.501 0.501 0.502 0.497 0.498 0.504 0.005 0.807 0.002 0.890 0.501
LAB_SLACK_ZNvDM M last 0.510 0.509 0.487 0.497 0.506 0.513 0.007 0.741 0.010 0.489 0.509
TOT_POYA_ZNvDM M last 0.516 0.516 0.483 0.497 0.513 0.519 0.064 0.003 0.037 0.009 0.516
XINF_NEG_ZNvDM M last 0.500 0.500 0.485 0.498 0.497 0.502 -0.024 0.258 -0.015 0.285 0.500

Naive PnL #

dix = dict_req_dm

sigx = dix["sigs"]
targ = dix["targs"][0]
cidx = dix["cidx"]
blax = dix["black"]
start = dix["start"]

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

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


dix["pnls"] = naive_pnl
dix = dict_req_dm

start = dix["start"]
cidx = dix["cidx"]
sig = dix["sigs"][0]

naive_pnl = dix["pnls"]
pnls = [sig + "_RAW"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="DM equity index futures relative value PnL, scaled to 10% annualized volatility",
    xcat_labels=["based on total macro score for each country (conceptual parity) relative to the DM average"],
    figsize=(16, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/31f1791d469d8a6cb2566d9967eee58a36bbd94c5b89a6ca450ad823e4f975f6.png
dix = dict_req_dm

start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][1:]

naive_pnl = dix["pnls"]
pnls = [s + "_RAW" for s in sigx]


lab={key + "_RAW": value for key, value in dict_themes_vdm.items()}
labels = dict(list(lab.items())[1:])


naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title=None,
    xcat_labels=labels,
    figsize=(18, 10),
)
https://macrosynergy.com/notebooks.build/trading-factors/cross-country-equity-futures-strategy/_images/c87c406f96c200343d0e77594c77fa9a3c676fb59a7f2afe6fef731a0479688a.png
dix = dict_req_dm
start = dix["start"]
sigx = dix["sigs"]

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

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)

labels={key + "_RAW": value for key, value in dict_themes_vdm.items()}
df_eval = df_eval.rename(columns=labels)

display(df_eval.transpose().astype("float").round(3))
Return % St. Dev. % Sharpe Ratio Sortino Ratio Max 21-Day Draw % Max 6-Month Draw % Peak to Trough Draw % Top 5% Monthly PnL Share USD_EQXR_NSA correl Traded Months
xcat
All macro themes (relative) 2.791 10.0 0.279 0.401 -8.903 -10.216 -24.197 1.084 -0.080 296.0
Ease of local finance (relative) 0.170 10.0 0.017 0.024 -10.439 -20.345 -35.201 14.210 0.073 296.0
Effective currency depreciation (relative) 0.358 10.0 0.036 0.050 -14.774 -13.272 -43.116 7.970 -0.019 296.0
Labor market slackening (relative) 0.611 10.0 0.061 0.087 -9.765 -16.448 -20.126 4.363 0.006 296.0
Terms of trade improvement (relative) 4.240 10.0 0.424 0.616 -13.235 -15.062 -27.626 0.869 -0.031 296.0
Inflation shortfall (relative) -1.353 10.0 -0.135 -0.190 -9.728 -19.909 -44.976 -2.076 -0.129 296.0