Pure macro FX strategies: the benefits of double diversification#

This notebook serves as an illustration of the points discussed in the post “Pure macro FX strategies: the benefits of double diversification” available on the Macrosynergy website. This post investigates a pure macro strategy for FX forward trading across developed and emerging countries based on an “external strength score” considering economic growth, external balances, and terms-of-trade.

This notebook provides the essential code required to replicate the analysis discussed in the post.

Get packages and JPMaQS data#

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

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

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

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

# Run only if needed!
!pip install git+https://github.com/macrosynergy/macrosynergy@develop
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os 

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

from macrosynergy.download import JPMaQSDownload

import warnings

warnings.simplefilter("ignore")

The JPMaQS indicators we consider are downloaded using the J.P. Morgan Dataquery API interface within the macrosynergy package. This is done by specifying ticker strings, formed by appending an indicator category code to a currency area code <cross_section>. These constitute the main part of a full quantamental indicator ticker, taking the form DB(JPMAQS,<cross_section>_<category>,<info>), where denotes the time series of information for the given cross-section and category. The following types of information are available:

value giving the latest available values for the indicator eop_lag referring to days elapsed since the end of the observation period mop_lag referring to the number of days elapsed since the mean observation period grade denoting a grade of the observation, giving a metric of real-time information quality.

After instantiating the JPMaQSDownload class within the macrosynergy.download module, one can use the download(tickers,start_date,metrics) method to easily download the necessary data, where tickers is an array of ticker strings, start_date is the first collection date to be considered and metrics is an array comprising the times series information to be downloaded. For more information see here.

# General cross-sections lists

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

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

# FX cross-sections lists

cids_nofx = ["EUR", "USD", "JPY", "SGD", "RUB"]  # not small or suitable for this analysis
cids_fx = list(set(cids) - set(cids_nofx))

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

cids_eur = ["CHF", "CZK", "HUF", "NOK", "PLN", "RON", "SEK"]  # trading against EUR
cids_eud = ["GBP", "TRY"]  # trading against EUR and USD
cids_usd = list(set(cids_fx) - set(cids_eur + cids_eud))  # trading against USD
# Category tickers

# External balances changes
xbds = [
    # Very short-term changes
    "MTBGDPRATIO_SA_3MMA_D1M1ML3",
    "CABGDPRATIO_SA_3MMA_D1M1ML3",
    "CABGDPRATIO_SA_1QMA_D1Q1QL1",
    # Short-term changes
    "MTBGDPRATIO_SA_6MMA_D1M1ML6",
    "CABGDPRATIO_SA_6MMA_D1M1ML6",
    "CABGDPRATIO_SA_2QMA_D1Q1QL2",
    # Medium-term changes
    "MTBGDPRATIO_SA_3MMAv60MMA",
    "CABGDPRATIO_SA_3MMAv60MMA",
    "CABGDPRATIO_SA_1QMAv20QMA",
]

# Economic growth trends and changes

gtds = [
    # Intutive growth estimates
    "INTRGDP_NSA_P1M1ML12_3MMA",
    "INTRGDPv5Y_NSA_P1M1ML12_3MMA",
    "INTRGDP_NSA_P1M1ML12_D3M3ML3",
    # Technical growth estimates
    "RGDPTECH_SA_P1M1ML12_3MMA",
    "RGDPTECHv5Y_SA_P1M1ML12_3MMA",
    "RGDPTECH_SA_P1M1ML12_D3M3ML3",
]

# Terms-of-trade changes
ttds = [
    # commodity-based changes
    "CTOT_NSA_P1M12ML1",
    "CTOT_NSA_P1M1ML12",
    "CTOT_NSA_P1M60ML1",
    # mixed dynamics
    "MTOT_NSA_P1M12ML1",
    "MTOT_NSA_P1M1ML12",
    "MTOT_NSA_P1M60ML1",
]

# Manufacturing confidence scores and changes
msds = [
    # Manufacturing confidence scores
    "MBCSCORE_SA",
    "MBCSCORE_SA_3MMA",
    # Short-term changes
    "MBCSCORE_SA_D3M3ML3",
    "MBCSCORE_SA_D1Q1QL1",
    # Medium-term changes
    "MBCSCORE_SA_D6M6ML6",
    "MBCSCORE_SA_D2Q2QL2",
]


main = xbds + gtds + ttds + msds


rets = [
    "FXXR_NSA",
    "FXXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
]

xcats = main + rets

# Extra tickers

xtix = ["USD_EQXR_NSA", "USD_GB10YXR_NSA"]

# Resultant tickers

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

JPMaQS indicators are conveniently grouped into 6 main categories: Economic Trends, Macroeconomic balance sheets, Financial conditions, Shocks and risk measures, Stylized trading factors, and Generic returns. Each indicator has a separate page with notes, description, availability, statistical measures, and timelines for main currencies. The description of each JPMaQS category is available under Macro quantamental academy. For tickers used in this notebook see External ratios trends, Terms-of-trade, Intuitive growth estimates, Technical real GDP trends, Manufacturing confidence scores, FX forward returns, and FX tradeability and flexibility.

start_date = "2000-01-01"
end_date = "2023-05-01"

# Retrieve credentials

client_id: str = os.getenv("DQ_OAUTH_CLIENT_ID")
client_secret: str = os.getenv("DQ_OAUTH_SECRET")

with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
    df = dq.download(
        tickers=tickers,
        start_date=start_date,
        end_date=end_date,
        suppress_warning=True,
        metrics=["all"],
        report_time_taken=True,
        show_progress=True,
        report_egress=True,
    )
Downloading data from JPMaQS.
Timestamp UTC:  2023-07-27 16:47:03
Connection successful!
Number of expressions requested: 3852
Requesting data: 100%|██████████| 193/193 [01:00<00:00,  3.20it/s]
Downloading data: 100%|██████████| 193/193 [02:13<00:00,  1.45it/s]
Time taken to download data: 	194.55 seconds.
Time taken to convert to dataframe: 	54.50 seconds.
Average upload size: 	0.20 KB
Average download size: 	499934.35 KB
Average time taken: 	22.27 seconds
Longest time taken: 	86.18 seconds
Average transfer rate : 	179616.97 Kbps
dfx = df.copy().sort_values(["cid", "xcat", "real_date"])
dfx.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 4599481 entries, 0 to 4599480
Data columns (total 7 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       
dtypes: datetime64[ns](1), float64(4), object(2)
memory usage: 280.7+ MB

Blacklist dictionaries#

Identifying and isolating periods of official exchange rate targets, illiquidity, or convertibility-related distortions in FX markets is the first step in creating an FX trading strategy. These periods can significantly impact the behavior and dynamics of currency markets, and failing to account for them can lead to inaccurate or misleading findings. A standard blacklist dictionary (fxblack in the cell below) can be passed to several macrosynergy package functions that exclude the blacklisted periods from related analyses.

dfb = df[df["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
    :, ["cid", "xcat", "real_date", "value"]
]
dfba = (
    dfb.groupby(["cid", "real_date"])
    .aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
    .reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
{'BRL': (Timestamp('2012-12-03 00:00:00'), Timestamp('2013-09-30 00:00:00')),
 'CHF': (Timestamp('2011-05-02 00:00:00'), Timestamp('2016-06-30 00:00:00')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
 'ILS': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-12-30 00:00:00')),
 'INR': (Timestamp('2000-01-03 00:00:00'), Timestamp('2004-12-31 00:00:00')),
 'MYR_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2007-11-30 00:00:00')),
 'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2023-05-01 00:00:00')),
 'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
 'RON': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2005-11-30 00:00:00')),
 'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2023-05-01 00:00:00')),
 'SGD': (Timestamp('2000-01-03 00:00:00'), Timestamp('2023-05-01 00:00:00')),
 'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
 'TRY_1': (Timestamp('2000-01-03 00:00:00'), Timestamp('2003-09-30 00:00:00')),
 'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2023-05-01 00:00:00'))}

Availability and renaming#

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

External balances changes#

msm.missing_in_df(df, xcats=xbds, cids=cids)
Missing xcats across df:  []
Missing cids for CABGDPRATIO_SA_1QMA_D1Q1QL1:  ['KRW', 'JPY', 'RON', 'CZK', 'PLN', 'TRY', 'BRL', 'GBP', 'EUR', 'PHP', 'THB']
Missing cids for CABGDPRATIO_SA_1QMAv20QMA:  ['KRW', 'JPY', 'RON', 'CZK', 'PLN', 'TRY', 'BRL', 'GBP', 'EUR', 'PHP', 'THB']
Missing cids for CABGDPRATIO_SA_2QMA_D1Q1QL2:  ['KRW', 'JPY', 'RON', 'CZK', 'PLN', 'TRY', 'BRL', 'GBP', 'EUR', 'PHP', 'THB']
Missing cids for CABGDPRATIO_SA_3MMA_D1M1ML3:  ['PEN', 'RUB', 'COP', 'HUF', 'CAD', 'ILS', 'SGD', 'USD', 'TWD', 'ZAR', 'MYR', 'RON', 'CHF', 'NZD', 'AUD', 'CLP', 'IDR', 'MXN', 'NOK', 'INR', 'SEK']
Missing cids for CABGDPRATIO_SA_3MMAv60MMA:  ['PEN', 'RUB', 'COP', 'HUF', 'CAD', 'ILS', 'SGD', 'USD', 'TWD', 'ZAR', 'MYR', 'RON', 'CHF', 'NZD', 'AUD', 'CLP', 'IDR', 'MXN', 'NOK', 'INR', 'SEK']
Missing cids for CABGDPRATIO_SA_6MMA_D1M1ML6:  ['PEN', 'RUB', 'COP', 'HUF', 'CAD', 'ILS', 'SGD', 'USD', 'TWD', 'ZAR', 'MYR', 'RON', 'CHF', 'NZD', 'AUD', 'CLP', 'IDR', 'MXN', 'NOK', 'INR', 'SEK']
Missing cids for MTBGDPRATIO_SA_3MMA_D1M1ML3:  []
Missing cids for MTBGDPRATIO_SA_3MMAv60MMA:  []
Missing cids for MTBGDPRATIO_SA_6MMA_D1M1ML6:  []
msm.check_availability(df, xcats=xbds, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_19_0.png

We rename the indicators with _...QMA extension (indicating Quarters Moving Average) in the group External ratios trends to _...MMA ( corresponding 3/6 months moving average) in order to align the naming conventions within the same group.

dict_repl = {
    "CABGDPRATIO_SA_1QMA_D1Q1QL1": "CABGDPRATIO_SA_3MMA_D1M1ML3",
    "CABGDPRATIO_SA_2QMA_D1Q1QL2": "CABGDPRATIO_SA_6MMA_D1M1ML6",
    "CABGDPRATIO_SA_1QMAv20QMA": "CABGDPRATIO_SA_3MMAv60MMA",
}

for key, value in dict_repl.items():
    dfx["xcat"] = dfx["xcat"].str.replace(key, value)
msm.check_availability(dfx, xcats=xbds, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_22_0.png

Terms-of-trade changes#

msm.missing_in_df(df, xcats=ttds, cids=cids)
Missing xcats across df:  []
Missing cids for CTOT_NSA_P1M12ML1:  []
Missing cids for CTOT_NSA_P1M1ML12:  []
Missing cids for CTOT_NSA_P1M60ML1:  []
Missing cids for MTOT_NSA_P1M12ML1:  []
Missing cids for MTOT_NSA_P1M1ML12:  []
Missing cids for MTOT_NSA_P1M60ML1:  []
msm.check_availability(df, xcats=ttds, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_28_0.png

Confidence scores and changes#

msm.missing_in_df(df, xcats=msds, cids=cids)
Missing xcats across df:  []
Missing cids for MBCSCORE_SA:  ['THB']
Missing cids for MBCSCORE_SA_3MMA:  ['MYR', 'THB', 'PHP', 'IDR']
Missing cids for MBCSCORE_SA_D1Q1QL1:  ['PEN', 'KRW', 'TWD', 'ZAR', 'JPY', 'RON', 'CZK', 'RUB', 'CHF', 'NZD', 'COP', 'HUF', 'CAD', 'GBP', 'EUR', 'AUD', 'CLP', 'SGD', 'MXN', 'NOK', 'PLN', 'ILS', 'TRY', 'BRL', 'THB', 'INR', 'USD', 'SEK']
Missing cids for MBCSCORE_SA_D2Q2QL2:  ['PEN', 'KRW', 'TWD', 'ZAR', 'JPY', 'RON', 'CZK', 'RUB', 'CHF', 'NZD', 'COP', 'HUF', 'CAD', 'GBP', 'EUR', 'AUD', 'CLP', 'SGD', 'MXN', 'NOK', 'PLN', 'ILS', 'TRY', 'BRL', 'THB', 'INR', 'USD', 'SEK']
Missing cids for MBCSCORE_SA_D3M3ML3:  ['MYR', 'THB', 'PHP', 'IDR']
Missing cids for MBCSCORE_SA_D6M6ML6:  ['MYR', 'THB', 'PHP', 'IDR']
msm.check_availability(df, xcats=msds, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_31_0.png

We rename the indicators with _...D1Q1QL1 and D2Q2QL2 extensions (indicating Quarterly data) in the group Manufacturing confidence scores to _...D3M3ML3 and _...D6M6ML6 ( corresponding 3/6 months) in order to align the naming conventions within the same group.

dict_repl = {
    "MBCSCORE_SA_D1Q1QL1": "MBCSCORE_SA_D3M3ML3",
    "MBCSCORE_SA_D2Q2QL2": "MBCSCORE_SA_D6M6ML6",
}

for key, value in dict_repl.items():
    dfx["xcat"] = dfx["xcat"].str.replace(key, value)
msm.check_availability(dfx, xcats=msds, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_34_0.png

Returns#

oths = rets
msm.missing_in_df(df, xcats=oths, cids=cids)
Missing xcats across df:  []
Missing cids for FXTARGETED_NSA:  ['USD']
Missing cids for FXUNTRADABLE_NSA:  ['USD']
Missing cids for FXXR_NSA:  ['USD']
Missing cids for FXXR_VT10:  ['USD']
msm.check_availability(df, xcats=oths, cids=cids, missing_recent=False)
_images/Pure macro FX strategies - the benefits of double diversification_37_0.png

Transformations and checks#

Features#

External ratio changes#

The macrosynergy package provides two useful functions, view_ranges() and view_timelines(), which facilitate the convenient visualization of data for selected indicators and cross-sections. These functions assist in plotting means, standard deviations, and time series of the chosen indicators. We use view_timelines() extensively in this notebook. Please see the corresponding section in the notebook Introduction to Macrosynergy package

xcatx = ["MTBGDPRATIO_SA_3MMA_D1M1ML3", "MTBGDPRATIO_SA_6MMA_D1M1ML6", "MTBGDPRATIO_SA_3MMAv60MMA"]
cidx = cids_fx
sdate = "2000-01-01"

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start=sdate,
    same_y=False,
    aspect=2,
    height=3,
    all_xticks=True,
    title="External balance ratio dynamics, all as changes of the % of GDP",
    title_fontsize=20,
    title_adj=1.01,
    label_adj=0.05,
)
_images/Pure macro FX strategies - the benefits of double diversification_42_0.png

Scoring#

The macrosynergy function make_zn_scores() normalizes 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 (0 in the cell below).

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 (we chose 5 years below).

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 (we choose monthly here). For more details and options please see Academy notebooks.

In the cell below the External ratio trends are zn-scored around zero value, using zero as the neutral value, 3 as the cutoff value for winsorization in terms of standard deviations, 5 years of minimum number of observations, and monthly re-estimation frequency. Since the categories are homogeneous across countries, we use the whole panel as the basis for the parameters rather than individual cross-section.

xbdx = [xc for xc in xbds if "Q" not in xc]

xcatx = xbdx
cidx = cids_fx

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,
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

We display the newly created z-scores for Short-term trade and current account balance trends, 3M/3M (MTBGDPRATIO_SA_3MMA_D1M1ML3) and Longer-term trade and current account balance trends vs 5 year average(MTBGDPRATIO_SA_3MMAv60MMA). These z-scores get extension _ZN to distinguish them from the original indicators.

xcatx = ["MTBGDPRATIO_SA_3MMA_D1M1ML3_ZN", "MTBGDPRATIO_SA_3MMAv60MMA_ZN"]
cidx = cids_fx
sdate = "2000-01-01"

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start=sdate,
    same_y=True,
    aspect=2,
    height=3,
    all_xticks=True,
    label_adj=0.05,
)
_images/Pure macro FX strategies - the benefits of double diversification_47_0.png

Business confidence changes#

As for the other groups - the External ratio changes and the Relative growth trends, we z-score the original business confidence indicators with macrosynergy’s make_zn_scores function and display the timelines for the selected 2 z-scores

msdx = ["MBCSCORE_SA_D3M3ML3", "MBCSCORE_SA_D6M6ML6"]

xcatx = xbdx
cidx = cids_fx

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

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

dfx = msm.update_df(dfx, dfa)
xcatx = ["MBCSCORE_SA_D6M6ML6_ZN", "MBCSCORE_SA_D3M3ML3_ZN"]
cidx = cids_fx
sdate = "2000-01-01"

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start=sdate,
    same_y=False,
    aspect=2,
    height=3,
    all_xticks=True,
    title="Changes in manufacturing confidence scores",
    title_fontsize=20,
    title_adj=1.01,
    label_adj=0.05,
)
_images/Pure macro FX strategies - the benefits of double diversification_59_0.png

Terms-of-trade changes#

As for previous groups of indicators (External ratio changes, Relative growth trends, and Business confidence changes), we z-score the Terms of trade changes, however, here we use here 50% of panel weight and 50% of individual cross-section since Terms of trade are not fully comparable. i.e. not homogenous across cross-sections. All other parameters are identical to the previous z-score calculations. The plot below displays z-scores for the commodity terms-of-trade dynamics

ttdx = ttds

xcatx = xbdx
cidx = cids_fx

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

for xc in ttdx:
    dfaa = msp.make_zn_scores(
        dfx,
        xcat=xc,
        cids=cidx,
        sequential=True,
        min_obs=261 * 5,
        neutral="zero",
        pan_weight=0.5,  # 50% cross-section weight as ToT changes are not fully comparable
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)
xcatx = [
    "CTOT_NSA_P1M12ML1_ZN",
    "CTOT_NSA_P1M1ML12_ZN",
    "CTOT_NSA_P1M60ML1_ZN",
]
cidx = cids_fx
sdate = "2000-01-01"

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start=sdate,
    same_y=False,
    aspect=2,
    height=3,
    all_xticks=True,
    title="Commodity terms-of-trade dynamics, z-scores",
    title_fontsize=20,
    title_adj=1.01,
    label_adj=0.05,
)
_images/Pure macro FX strategies - the benefits of double diversification_63_0.png

External strength score#

In this section, we combine the z-scores within each group to create Macro trend scores for

  • external balances,

  • economic growth (combining Intuitive growth estimates, Technical real GDP trends, and Manufacturing confidence scores), and

  • terms-of-trade

with the help of linear_composite() function from the macrosynergy package

xbdz = [xc + "_ZN" for xc in xbdx]
grdz = [xc + "_ZN" for xc in grdx] + [xc + "_ZN" for xc in msdx]
ttdz = [xc + "_ZN" for xc in ttdx]
dict_css = {
    "XBT_ALL_CZS": xbdz,
    "EGT_ALL_CZS": grdz,
    "TTD_ALL_CZS": ttdz,
}

xs =list(dict_css.keys())
cidx = cids_fx

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

for key, value in dict_css.items():
    dfaa = msp.linear_composite(
        df=dfx,
        xcats=value,
        cids=cidx,
        complete_xcats=False,
        new_xcat=key,
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

The new indicators then are re-scored again

xcatx = xs
cidx = cids_fx

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,
        thresh=3,
        postfix="_ZN",
        est_freq="m",
    )
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

The plots below display Macro trend scores for external balances, economic growth, and terms-of-trade for each cross-section

xsz = [xc + "_ZN" for xc in xs]

xcatx = xsz
cidx = cids_fx
sdate = "2000-01-01"

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start=sdate,
    same_y=True,
    aspect=2,
    height=3,
    all_xticks=True,
    title="Macro trend scores for external balances, economic growth, and terms-of-trade",
    xcat_labels=["External balances trends", "Economic growth trends", "Terms-of-trade trends"],
    title_fontsize=20,
    title_adj=1.01,
    label_adj=0.05,
)
_images/Pure macro FX strategies - the benefits of double diversification_72_0.png

The below correlation matrix calculated with correl_matrix() function from the macrosynergy package shows monthly correlations across all constituents of the macro trends used in this post. These indicators are block-wise positively correlated, i.e., within the trend category they represent. However, there is not much correlation across blocks and even a negative tilt of correlation between growth trend indicators and external balance trend indicators. This illustrates the potential for diversification across concepts. Additional macro concepts could be applied to the present pure macro-FX strategy, such as labor market tightness, producer price growth, credit growth, or international investment positions. Beyond plausibility and evidence for direct predictive power, an important criterion for extending a model is low correlation and different seasons relative to the existing set.

xcatx = xbds + grds + msds + ttds

msp.correl_matrix(
    dfx,
    xcats=xcatx,
    freq="M",
    cids=cids_fx,
    size=(14, 8),
    cluster=True,
    title="Macro trend indicators: correlation matrix for 26 countries (2000 - 2023)",
)
_images/Pure macro FX strategies - the benefits of double diversification_74_0.png

Targets#

We choose as target FXXR_VT10 (Vol-targeted FX forward return). Please see here for the description of FX forward returns indicators

Below correlation matrix of volatility targeted weekly FX forward returns across the 26 developed and emerging market currencies sows mostly positive correlation between returns, however, the coefficients are mostly below 50% and often near zero or negative.

msp.correl_matrix(
    df,
    xcats="FXXR_VT10",
    freq="W",
    cids=cids_fx,
    size=(14, 8),
    cluster=False,
    title="Weekly FX returns (on vol-targeted positions): correlation matrix (2000 - 2023)",
)
_images/Pure macro FX strategies - the benefits of double diversification_86_0.png

Value checks#

External strength#

Global composites#

Specs and panel tests#

This section uses extensively the following classes of the macrosynergy package:

sigs = [xc for xc in set(xs_czs + xsz) if "ALL" in xc]

ms = 'XS_ALL_CZS'  # main signal
os = list(set(sigs) - set([ms]))  # other signals

targ = 'FXXR_VT10'
cidx = cids_fx
start = "2000-01-01"

dict_xs = {
    "sig": ms,
    "rivs": os,
    "targ": targ,
    "cidx": cidx,
    "start": start,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}

Panel regression shows a positive relation between end-of-month information states of external strength and subsequent weekly and monthly vol-targeted FX returns. The test suggests that the probability of this relation being systematic rather than accidental is around 99%.

dix = dict_xs

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

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    start=start,
    blacklist=blax,
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="External strength score, equally weighted across external balances, growth, and terms-of-trade",
    ylab="Next month FX forward return, vol-targeted position (10% ar)",
    title="Panel test of external strength score as a predictor of FX returns",
    size=(10, 6),
    prob_est="map",
)
_images/Pure macro FX strategies - the benefits of double diversification_94_0.png

Accuracy and correlation check#

dix = dict_xs

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

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_xs
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval
Panel 0.525 0.521 0.552 0.547 0.565 0.476 0.048 0.000 0.032 0.000
Mean years 0.524 0.510 0.546 0.548 0.559 0.461 0.025 0.428 0.014 0.516
Positive ratio 0.708 0.583 0.625 0.708 0.750 0.375 0.583 0.458 0.583 0.333
Mean cids 0.524 0.518 0.548 0.549 0.567 0.469 0.042 0.389 0.027 0.430
Positive ratio 0.654 0.692 0.577 0.923 0.846 0.192 0.769 0.462 0.731 0.462
dix = dict_xs
srrx = dix["srr"]
srr.accuracy_bars(type="cross_section", size=(14, 5))
_images/Pure macro FX strategies - the benefits of double diversification_98_0.png
dix = dict_xs
srrx = dix["srr"]
srr.accuracy_bars(type="years", size=(14, 5))
_images/Pure macro FX strategies - the benefits of double diversification_99_0.png

The table and bars below compare the accuracy of the composite external strength score with its constituents (external balances, economic growth, and terms-of-trade)

dix = dict_xs
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval
EGT_ALL_CZS_ZN 0.517 0.510 0.572 0.546 0.555 0.465 0.042 0.001 0.023 0.004
TTD_ALL_CZS_ZN 0.509 0.512 0.475 0.548 0.560 0.464 0.030 0.016 0.017 0.040
XBT_ALL_CZS_ZN 0.517 0.518 0.492 0.547 0.565 0.471 0.022 0.066 0.015 0.063
XS_ALL_CZS 0.525 0.521 0.552 0.547 0.565 0.476 0.048 0.000 0.032 0.000
srr.accuracy_bars(type="signals", size=(10, 4))
_images/Pure macro FX strategies - the benefits of double diversification_102_0.png

Naive PnL#

dix = dict_xs

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
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", "USD_GB10YXR_NSA", "EUR_FXXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        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(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl

We estimate the economic value of a composite external strength score based on a naïve PnL computed according to a standard procedure used in Macrosynergy research posts. A naive PnL is calculated for simple monthly rebalancing in accordance with the external strength score at the end of each month as the basis for the positions of the next month and under consideration of a 1-day slippage for trading. The trading signals are capped at 2 standard deviations in either direction for each currency as a reasonable risk limit, and applied to volatility-targeted positions. This means that one unit of signal translates into one unit of risk (approximated by estimated return volatility) for each currency. The naïve PnL does not consider transaction costs or compounding. For the chart below, the PnL has been scaled to an annualized volatility of 10%

dix = dict_xs

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sig"]]

naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Naive PnLs of FX forward positions, 26 countries, monthly rebalancing",
    xcat_labels=["External strength score", "Long only"],
    figsize=(16, 8),
)
_images/Pure macro FX strategies - the benefits of double diversification_106_0.png

All three types of macro trends have contributed to PnL generation but in different “seasons.” Economic growth trends played an important role in predictive FX trends in the 2000s but have generated only modest value since 2010. Conversely, external trade trends produced no value in the 2000s but greatly added to PnL generation in the 2010s and 2020s. The mirror image of these probably is not accidental: in times of strong international capital flows, high-growth economies tend to attract FX inflows even if their external balances deteriorate, while in times of financial shocks and de-globalization, external deficits are a greater concern. The important point is that jointly these two trends produced consistent value. Finally, terms-of-trades have helped PnL generation across decades, but naturally only in episodes where international commodity prices changed significantly.

dix = dict_xs

start = dix["start"]
cidx = dix["cidx"]
sigx = dix["rivs"]

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Macro trend-based PnLs of FX forward positions, 26 countries, monthly rebalancing",
    xcat_labels=["External balances trend", "Economic growth trend", "Terms-of-trade trend"],
    figsize=(16, 8),
)
_images/Pure macro FX strategies - the benefits of double diversification_108_0.png
dix = dict_xs

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

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)
display(df_eval.transpose())
Return (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw Traded Months
xcat
EGT_ALL_CZS_ZN_PZN 6.882883 10.0 0.688288 0.990517 -20.356797 -18.187066 281
TTD_ALL_CZS_ZN_PZN 4.181472 10.0 0.418147 0.598446 -13.81937 -19.489624 281
XBT_ALL_CZS_ZN_PZN 3.297261 10.0 0.329726 0.468104 -13.412194 -31.901496 281
XS_ALL_CZS_PZN 7.966966 10.0 0.796697 1.155394 -15.38805 -24.614439 281
dix = dict_xs

start = dix["start"]
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN", freq="q", start=start, figsize=(16, 7)
)
_images/Pure macro FX strategies - the benefits of double diversification_110_0.png

Developed markets#

Specs and panel tests#

Here we consider a simple trading strategy based on the external strength score and on its constituents for the 7 developed market currencies alone [cids_dmfx =’GBP’, ‘AUD’, ‘NOK’, ‘SEK’, ‘NZD’, ‘CAD’, ‘CHF’]. Trading developed markets with macro trends may be more convenient and has also been profitable. However, the below chart shows that a developed market FX strategy would have produced less than half the risk-adjusted return of the global portfolio, with a Sharpe ratio of just 0.33., and greater seasonality.

sigs = [xc for xc in set(xs_czs + xsz)]

ms = 'XS_ALL_CZS'  # main signal
os = list(set(sigs) - set([ms]))  # other signals

targ = 'FXXR_VT10'
cidx = cids_dmfx
start = "2000-01-01"

dict_xsdm = {
    "sig": ms,
    "rivs": os,
    "targ": targ,
    "cidx": cidx,
    "start": start,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}
dix = dict_xsdm

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

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    start=start,
    blacklist=blax,
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab=None,
    ylab=None,
    title=None,
    size=(10, 6),
    prob_est="map",
)
_images/Pure macro FX strategies - the benefits of double diversification_115_0.png

Accuracy and correlation check#

dix = dict_xsdm

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

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_xsdm
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval
Panel 0.509 0.507 0.567 0.521 0.527 0.487 0.041 0.072 0.020 0.183
Mean years 0.509 0.498 0.560 0.518 0.517 0.480 0.019 0.475 0.003 0.526
Positive ratio 0.625 0.458 0.500 0.458 0.500 0.500 0.625 0.333 0.542 0.292
Mean cids 0.509 0.503 0.564 0.521 0.525 0.482 0.044 0.458 0.018 0.591
Positive ratio 0.571 0.571 0.714 0.857 0.571 0.143 0.714 0.429 0.714 0.429
dix = dict_xsdm
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval
EGT_ALL_CZS_ZN 0.519 0.520 0.486 0.521 0.541 0.498 0.071 0.002 0.043 0.005
TTD_ALL_CZS_ZN 0.512 0.509 0.552 0.521 0.530 0.489 0.028 0.219 0.017 0.273
XBT_ALL_CZS_ZN 0.498 0.498 0.517 0.521 0.519 0.477 -0.010 0.665 -0.014 0.364
XS_ALL_CZS 0.509 0.507 0.567 0.521 0.527 0.487 0.041 0.072 0.020 0.183
XS_GRTT_CZS 0.521 0.518 0.556 0.521 0.537 0.499 0.062 0.007 0.041 0.008
XS_XBGR_CZS 0.500 0.499 0.515 0.521 0.520 0.478 0.036 0.115 0.011 0.475
XS_XBTT_CZS 0.511 0.508 0.556 0.521 0.528 0.488 0.013 0.561 0.007 0.670

Naive PnL#

dix = dict_xsdm

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
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", "USD_GB10YXR_NSA"],
)

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

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

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sig"]]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx] + ["Long only"]

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Naive PnLs of FX forward positions, 7 developed countries, monthly rebalancing",
    xcat_labels=["External strength score", "Long only"],
    figsize=(16, 8),
)
_images/Pure macro FX strategies - the benefits of double diversification_122_0.png
dix = dict_xsdm

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sig"]] + [s for s in dix['rivs'] if "ALL" in s]

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Macro trend-based PnLs of FX positions, 7 developed countries, monthly rebalancing",
    xcat_labels=["External strength score", "External balances trend", "Economic growth trend", "Terms-of-trade trend"],
    figsize=(16, 8),
)
_images/Pure macro FX strategies - the benefits of double diversification_123_0.png
dix = dict_xsdm

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

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)
display(df_eval.transpose())
Return (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw Traded Months
xcat
EGT_ALL_CZS_ZN_PZN 5.57679 10.0 0.557679 0.800168 -10.951806 -15.485117 281
TTD_ALL_CZS_ZN_PZN 2.313679 10.0 0.231368 0.323478 -13.303108 -18.448901 281
XBT_ALL_CZS_ZN_PZN -0.88085 10.0 -0.088085 -0.122554 -15.71035 -26.665989 281
XS_ALL_CZS_PZN 3.494037 10.0 0.349404 0.488466 -17.333353 -12.871436 281
XS_GRTT_CZS_PZN 4.80821 10.0 0.480821 0.673755 -14.759377 -13.893476 281
XS_XBGR_CZS_PZN 3.015365 10.0 0.301537 0.424921 -16.74158 -17.138939 281
XS_XBTT_CZS_PZN 1.096477 10.0 0.109648 0.153394 -15.868891 -17.639191 281

Individual signals#

Specs and panel tests#

allzs = xbdz + ttdz + grdz
sigs = allzs

ms = 'XS_ALL_CZS'  # main signal
os = list(set(sigs) - set([ms]))  # other signals

targ = 'FXXR_VT10'
cidx = set(cids_fx) - set(["THB", "RON"])  # countries which do not have all the data
start = "2000-01-01"

dict_allzs = {
    "sig": ms,
    "rivs": os,
    "targ": targ,
    "cidx": cidx,
    "start": start,
    "black": fxblack,
    "srr": None,
    "pnls": None,
}

Accuracy and correlation check#

dix = dict_allzs

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

srr = mss.SignalReturnRelations(
    dfx,
    cids=cidx,
    sig=sig,
    rival_sigs=rivs,
    sig_neg=False,
    ret=targ,
    freq="M",
    start=start,
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_allzs
srrx = dix["srr"]
display(srrx.signals_table().sort_index().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval
CABGDPRATIO_SA_3MMA_D1M1ML3_ZN 0.510 0.510 0.496 0.543 0.553 0.466 0.020 0.116 0.010 0.271
CABGDPRATIO_SA_3MMAv60MMA_ZN 0.514 0.512 0.532 0.543 0.554 0.469 0.013 0.332 0.010 0.249
CABGDPRATIO_SA_6MMA_D1M1ML6_ZN 0.493 0.493 0.498 0.543 0.536 0.450 0.002 0.855 -0.005 0.540
CTOT_NSA_P1M12ML1_ZN 0.509 0.512 0.470 0.544 0.557 0.467 0.031 0.014 0.017 0.050
CTOT_NSA_P1M1ML12_ZN 0.514 0.517 0.464 0.544 0.563 0.472 0.026 0.037 0.018 0.031
CTOT_NSA_P1M60ML1_ZN 0.513 0.517 0.457 0.544 0.563 0.471 0.028 0.028 0.017 0.044
INTRGDP_NSA_P1M1ML12_3MMAvBM_ZN 0.525 0.511 0.690 0.542 0.549 0.473 0.032 0.012 0.029 0.001
INTRGDPv5Y_NSA_P1M1ML12_3MMAvBM_ZN 0.507 0.509 0.474 0.541 0.550 0.468 0.025 0.050 0.016 0.072
MBCSCORE_SA_D3M3ML3_ZN 0.498 0.499 0.492 0.546 0.545 0.453 0.019 0.161 0.011 0.232
MBCSCORE_SA_D6M6ML6_ZN 0.500 0.501 0.486 0.546 0.547 0.455 0.022 0.102 0.008 0.366
MTBGDPRATIO_SA_3MMA_D1M1ML3_ZN 0.509 0.510 0.479 0.544 0.555 0.466 0.024 0.055 0.015 0.080
MTBGDPRATIO_SA_3MMAv60MMA_ZN 0.512 0.512 0.499 0.544 0.556 0.468 0.022 0.086 0.014 0.101
MTBGDPRATIO_SA_6MMA_D1M1ML6_ZN 0.516 0.517 0.491 0.544 0.561 0.473 0.038 0.003 0.023 0.007
MTOT_NSA_P1M12ML1_ZN 0.501 0.501 0.500 0.545 0.546 0.456 0.020 0.122 0.012 0.163
MTOT_NSA_P1M1ML12_ZN 0.505 0.506 0.496 0.545 0.551 0.461 0.019 0.137 0.011 0.186
MTOT_NSA_P1M60ML1_ZN 0.519 0.517 0.528 0.541 0.557 0.477 0.021 0.118 0.016 0.065
RGDPTECH_SA_P1M1ML12_3MMAvBM_ZN 0.527 0.511 0.702 0.544 0.551 0.471 0.024 0.069 0.021 0.019
RGDPTECHv5Y_SA_P1M1ML12_3MMAvBM_ZN 0.494 0.501 0.421 0.543 0.544 0.458 0.022 0.103 0.010 0.273
XS_ALL_CZS 0.526 0.521 0.559 0.544 0.562 0.479 0.048 0.000 0.032 0.000

Naive PnL#

The diversified risk parity signal would have outperformed not only all three major macro trend signals but also each and every signal based on any of the 18 underlying constituents. The chart below compares the performance naïve PnLs of the parity-based diversified external strength score and all trend constituents, i.e., all the individual quantamental series (normalized) behind the three main macro trends. Even the best score chosen with hindsight (merchandise trade balance trend) would only have produced a Sharpe of 0.56 versus 0.77 for the composite.

dix = dict_allzs

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
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", "USD_GB10YXR_NSA"],
)

for sig in sigx:
    naive_pnl.make_pnl(
        sig,
        sig_neg=False,
        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(vol_scale=10, label="Long only")
dix["pnls"] = naive_pnl
dix = dict_allzs

start = dix["start"]
cidx = dix["cidx"]
sigx = [dix["sig"]] + dix["rivs"]

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
    title="Macro trend-based PnLs of FX forward positions, 26 countries, monthly rebalancing",
    xcat_labels=["Risk-parity composite"]
    + ["Constituent " + str(i + 1) for i in range(len(dix["rivs"]))],
    figsize=(16, 10),
)
_images/Pure macro FX strategies - the benefits of double diversification_134_0.png
dix = dict_allzs

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

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start=start,
)
display(df_eval.transpose())
Return (pct ar) St. Dev. (pct ar) Sharpe Ratio Sortino Ratio Max 21-day draw Max 6-month draw Traded Months
xcat
CABGDPRATIO_SA_3MMA_D1M1ML3_ZN_PZN 2.437138 10.0 0.243714 0.354306 -12.048642 -16.870024 281
CABGDPRATIO_SA_3MMAv60MMA_ZN_PZN 2.541556 10.0 0.254156 0.353254 -14.859494 -32.940338 281
CABGDPRATIO_SA_6MMA_D1M1ML6_ZN_PZN -0.373994 10.0 -0.037399 -0.052398 -20.73413 -23.404099 281
CTOT_NSA_P1M12ML1_ZN_PZN 4.714923 10.0 0.471492 0.674996 -11.560992 -23.163421 281
CTOT_NSA_P1M1ML12_ZN_PZN 4.639186 10.0 0.463919 0.664457 -11.210243 -22.024057 281
CTOT_NSA_P1M60ML1_ZN_PZN 4.483089 10.0 0.448309 0.648494 -13.975622 -18.829753 281
INTRGDP_NSA_P1M1ML12_3MMAvBM_ZN_PZN 4.499209 10.0 0.449921 0.642913 -20.632752 -36.220192 281
INTRGDPv5Y_NSA_P1M1ML12_3MMAvBM_ZN_PZN 3.636638 10.0 0.363664 0.524063 -12.421657 -18.306499 281
MBCSCORE_SA_D3M3ML3_ZN_PZN 1.970313 10.0 0.197031 0.279447 -17.533617 -28.912365 281
MBCSCORE_SA_D6M6ML6_ZN_PZN 2.031413 10.0 0.203141 0.28751 -20.077759 -36.935549 281
MTBGDPRATIO_SA_3MMA_D1M1ML3_ZN_PZN 3.419537 10.0 0.341954 0.493461 -12.140268 -16.637656 281
MTBGDPRATIO_SA_3MMAv60MMA_ZN_PZN 4.218235 10.0 0.421824 0.595952 -14.405121 -29.982225 281
MTBGDPRATIO_SA_6MMA_D1M1ML6_ZN_PZN 5.612105 10.0 0.561211 0.806913 -14.24829 -23.317975 281
MTOT_NSA_P1M12ML1_ZN_PZN 2.918412 10.0 0.291841 0.415893 -10.236556 -23.034866 281
MTOT_NSA_P1M1ML12_ZN_PZN 2.670606 10.0 0.267061 0.378117 -10.703241 -17.456486 281
MTOT_NSA_P1M60ML1_ZN_PZN 3.035244 10.0 0.303524 0.427329 -12.254852 -18.602538 281
RGDPTECH_SA_P1M1ML12_3MMAvBM_ZN_PZN 4.029156 10.0 0.402916 0.58061 -24.888907 -49.628256 281
RGDPTECHv5Y_SA_P1M1ML12_3MMAvBM_ZN_PZN 2.503662 10.0 0.250366 0.361535 -14.11456 -35.148049 281
XS_ALL_CZS_PZN 7.660575 10.0 0.766058 1.106845 -15.889593 -23.121354 281