Modified and balanced FX carry #
This notebook serves as an illustration of the points discussed in the post [“This notebook serves as an illustration of the points discussed in the post “Modified and balanced FX carry” available on the Macrosynergy website.
The post demonstrates two simple ways to enhance FX carry strategies with economic information. The first way increases or reduces the carry signal depending on whether relevant economic indicators reinforce or contradict its direction. The output can be called “modified carry”. It is a gentle adjustment that leaves the basic characteristics of the original carry strategy intact. The second method equalizes the influence of carry and economic indicators, thus diversifying over signals with complementary strengths. The combined signal can be called “balanced carry”. An empirical analysis of carry modification and balancing with economic performance indicators for 26 countries since 2000 suggests that both adjustments would have greatly improved the performance of voltargeted carry strategies. Modified carry would also have improved the performance of hedged FX carry strategies.
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 macrosynergy upgrade
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import math
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
DB(JPMAQS,<cross_section>_<category>,<info>)
, where
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 realtime 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
.
# Crosssections
cids_dmlc = ["EUR", "JPY", "USD"] # DM large currency areas
cids_dmsc = ["AUD", "CAD", "CHF", "GBP", "NOK", "NZD", "SEK"] # DM small currency areas
cids_latm = ["BRL", "COP", "CLP", "MXN", "PEN"] # Latam
cids_emea = ["CZK", "HUF", "ILS", "PLN", "RON", "RUB", "TRY", "ZAR"] # EMEA
cids_emas = ["IDR", "INR", "KRW", "MYR", "PHP", "THB", "TWD"] # EM Asia flex
cids_apeg = ["CNY", "HKD", "SGD"] # EM Asia peg
cids = cids_dmlc + cids_dmsc + cids_latm + cids_emea + cids_emas
cids_fx = ["JPY"] + cids_dmsc + cids_latm + cids_emea + cids_emas
cids_fxx = list(set(cids_fx)  set(["IDR", "INR"]))
cids_dmfx = ["JPY"] + cids_dmsc
cids_emfx = list(set(cids_fx)  set(cids_dmfx))
cids_eur = ["CHF", "CZK", "HUF", "NOK", "PLN", "RON", "SEK"] # trading against EUR
cids_eus = ["GBP", "RUB", "TRY"] # trading against EUR and USD
cids_eud = ["GBP", "RUB", "TRY"] # trading against EUR and USD
cids_usd = list(set(cids_fx)  set(cids_eur + cids_eus)) # trading against USD
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 , External balance ratios , International investment position , Intuitive growth estimates , FX forward carry , Industrial production trends , Labor market dynamics , Labor market tightness , FX forward returns , FX tradeability and flexibility , and Equity index future returns .
# Categories
main = [
"FXCRY_NSA",
"FXCRY_VT10",
"FXCRYHvGDRB_NSA",
"FXCRR_NSA",
"FXCRR_VT10",
"FXCRRHvGDRB_NSA",
]
xtra = [
"BXBGDPRATIO_NSA_12MMA",
"CABGDPRATIO_NSA_12MMA",
"MTBGDPRATIO_NSA_12MMA_D1M1ML3",
"MTBGDPRATIO_SA_3MMAv60MMA",
"NIIPGDP_NSA",
"INTRGDP_NSA_P1M1ML12_3MMA",
"IP_SA_P1M1ML12_3MMA",
"EMPL_NSA_P1M1ML12_3MMA",
"EMPL_NSA_P1Q1QL4",
"UNEMPLRATE_SA_3MMAv5YMA",
]
rets = [
"FXTARGETED_NSA",
"FXUNTRADABLE_NSA",
"FXXR_NSA",
"FXXR_VT10",
"FXXRHvGDRB_NSA",
"EQXR_NSA",
]
xcats = main + xtra + rets
# Resultant tickers
tickers = [cid + "_" + xcat for cid in cids for xcat in xcats]
print(f"Maximum number of tickers is {len(tickers)}")
Maximum number of tickers is 660
# Download series from J.P. Morgan DataQuery by tickers. to speed up running time, only using 3 years
start_date = "20000101"
end_date = "20230501"
# Retrieve credentials
client_id: str = os.getenv("DQ_CLIENT_ID")
client_secret: str = os.getenv("DQ_CLIENT_SECRET")
with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
df = dq.download(
tickers=tickers,
start_date=start_date,
end_date=end_date,
suppress_warning=True,
metrics=["all"],
report_time_taken=True,
show_progress=True,
)
Downloading data from JPMaQS.
Timestamp UTC: 20240321 15:09:35
Connection successful!
Requesting data: 100%██████████ 132/132 [00:31<00:00, 4.24it/s]
Downloading data: 100%██████████ 132/132 [00:26<00:00, 5.06it/s]
Time taken to download data: 67.14 seconds.
Some expressions are missing from the downloaded data. Check logger output for complete list.
228 out of 2640 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()`.
Blacklist dictionary #
Identifying and isolating periods of official exchange rate targets, illiquidity, or convertibilityrelated distortions in FX markets is the first step in creating an FX trading strategy. These periods can significantly impact the behavior and dynamics of currency markets, and failing to account for them can lead to inaccurate or misleading findings.
dfb = df[df["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
:, ["cid", "xcat", "real_date", "value"]
]
dfba = (
dfb.groupby(["cid", "real_date"])
.aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
.reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
{'BRL': (Timestamp('20121203 00:00:00'), Timestamp('20130930 00:00:00')),
'CHF': (Timestamp('20111003 00:00:00'), Timestamp('20150130 00:00:00')),
'CZK': (Timestamp('20140101 00:00:00'), Timestamp('20170731 00:00:00')),
'ILS': (Timestamp('20000103 00:00:00'), Timestamp('20051230 00:00:00')),
'INR': (Timestamp('20000103 00:00:00'), Timestamp('20041231 00:00:00')),
'MYR_1': (Timestamp('20000103 00:00:00'), Timestamp('20071130 00:00:00')),
'MYR_2': (Timestamp('20180702 00:00:00'), Timestamp('20230501 00:00:00')),
'PEN': (Timestamp('20210701 00:00:00'), Timestamp('20210730 00:00:00')),
'RON': (Timestamp('20000103 00:00:00'), Timestamp('20051130 00:00:00')),
'RUB_1': (Timestamp('20000103 00:00:00'), Timestamp('20051130 00:00:00')),
'RUB_2': (Timestamp('20220201 00:00:00'), Timestamp('20230501 00:00:00')),
'THB': (Timestamp('20070101 00:00:00'), Timestamp('20081128 00:00:00')),
'TRY_1': (Timestamp('20000103 00:00:00'), Timestamp('20030930 00:00:00')),
'TRY_2': (Timestamp('20200101 00:00:00'), Timestamp('20230501 00:00:00'))}
Availability #
It is important to assess data availability before conducting any analysis. It allows identifying 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 crosssection is available as well as determining the appropriate time periods for analysis.
msm.check_availability(df, xcats=main, cids=cids)
Transformations and checks #
Real carry metrics #
Using rolling medians can be a useful approach to mitigate the undue influence of shortterm (untradable) carry distortions in financial data. By calculating the median over a rolling window of observations, we obtain a more robust measure of central tendency that is less affected by extreme values or outliers. We use 5day rolling medians, which aligns with market conventions. This means that for each day, we calculate the median of the previous 5 days’ observations. Using rolling medians can help to eliminate shortterm (untradeable) carry distortions due to various factors, such as temporary market shocks, liquidity issues, or unusual trading activity. These distortions can have a significant impact on individual data points, leading to misleading results.
calcs = [
"FXCRY_NSABL5DM = ( FXCRY_NSA ).rolling(5).median()",
"FXCRR_NSABL5DM = ( FXCRR_NSA ).rolling(5).median()",
"FXCRR_VT10BL5DM = ( FXCRR_VT10 ).rolling(5).median()",
"FXCRRHvGDRB_NSABL5DM = ( FXCRRHvGDRB_NSA ).rolling(5).median()",
]
dfa = msp.panel_calculator(df, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(df, dfa)
As the first step, part of the preliminary analysis, we display FX carry (Nominal forwardimplied carry vs. dominant cross) and 5 days rolling median real carry versus dominant cross (with hedges). Please see here for the definition of the indicators and Introduction to Macrosynergy package for the standard functions used throughout this notebook.
xcats_sel = ["FXCRR_NSA", "FXCRR_NSABL5DM"]
msp.view_ranges(
dfx,
cids=cids_fx,
xcats=xcats_sel,
kind="bar",
sort_cids_by="mean",
title=None,
ylab="% annualized rate",
start="20020101",
)
msp.view_timelines(
dfx,
xcats=xcats_sel,
cids=cids_fx,
ncol=4,
cumsum=False,
start="20020101",
same_y=False,
size=(12, 12),
all_xticks=True,
title=None,
xcat_labels=None,
)
Carry adjustments #
Preparations #
For convenience, we use the negative of the unemployment rate and replace the name of quarterly employment growth
EMPL_NSA_P1Q1QL4
with
EMPL_NSA_P1M1ML12_3MMA
calcs = [f"UNEMPLRATE_SA_3MMAv5YMAN =  UNEMPLRATE_SA_3MMAv5YMA"]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids)
dfx = msm.update_df(dfx, dfa)
dfx["xcat"] = dfx["xcat"].str.replace("EMPL_NSA_P1Q1QL4", "EMPL_NSA_P1M1ML12_3MMA")
Differentials to benchmark #

Relative intuitive GDP growth: This is the difference between quantamental indicators of the estimated GDP growth trend in the reference currency area and the natural benchmark area, i.e. the U.S. or the euro area. Growth trend here means the latest estimable percent change over a year ago in 3month moving averages (to mitigate monthly volatility). It is based on JPMaQS technical intuitive GDP trends, which are sequential realtime estimates based on regressions that use the latest available national accounts data and monthlyfrequency activity data. See the relevant documentation on the Academy site .

Relative industrial production growth: This is the difference between quantamental indicators of reported industrial output trends in the reference currency area and the natural benchmark country. Production trend is again measured as % over a year ago in 3month moving averages. Industrial production focuses on tradable goods, for which the exchange rate is particularly important. See also the related documentation on the Academy site

Relative employment growth: This is the difference between quantamental indicators of reported employment trends in the reference currency area and the natural benchmark country. Again, the growth trends are measured as % over a year ago in 3month moving averages. In some countries, employment data are only available at a quarterly frequency and those values are used instead of 3month averages. See the related documentation on the Academy site .

Relative unemployment gaps: This is the difference between quantamental indicators of reported unemployment gaps in the reference currency area and the natural benchmark country. An unemployment gap here means the difference between the latest unemployment rate, seasonally adjusted and as 3month rolling average or quarterly values, and its 5year moving average. It is put in negative terms, as low unemployment means economic strength, and, unlike employment growth, is a measure of labor market tightness. See the relevant documentation .
We calculate differentials to benchmarks for three types of currencies: those trading against USD, EUR and both USD and EUR. The list of currencies is as follows:

Currencies traded against EUR: [“CHF”, “CZK”, “HUF”, “NOK”, “PLN”, “RON”, “SEK”]

Currencies traded against USD and EUR: [“GBP”, “RUB”, “TRY”]. The benchmark equally weighs USD and EUR data

Currencies traded against USD: all other currencies in our dataset
pafs = [
"INTRGDP_NSA_P1M1ML12_3MMA",
"IP_SA_P1M1ML12_3MMA",
"EMPL_NSA_P1M1ML12_3MMA",
"UNEMPLRATE_SA_3MMAv5YMAN",
]
xcatx = pafs
for xc in xcatx:
calc_eur = [f"{xc}vBM = {xc}  iEUR_{xc}"]
calc_usd = [f"{xc}vBM = {xc}  iUSD_{xc}"]
calc_eud = [f"{xc}vBM = {xc}  0.5 * ( iEUR_{xc} + iUSD_{xc} )"]
dfa_eur = msp.panel_calculator(
dfx,
calcs=calc_eur,
cids=cids_eur, # blacklist=fxblack
)
dfa_usd = msp.panel_calculator(
dfx,
calcs=calc_usd,
cids=cids_usd, # blacklist=fxblack
)
dfa_eud = msp.panel_calculator(
dfx,
calcs=calc_eud,
cids=cids_eud, # blacklist=fxblack
)
dfa = pd.concat([dfa_eur, dfa_usd, dfa_eud])
dfx = msm.update_df(dfx, dfa)
The resulting relative growth trends (Relative intuitive GDP growth and Relative industrial production growth as described above) are displayed with the help of customized function
view_timelines()
from the
macrosynergy
package:
xcatx = ["INTRGDP_NSA_P1M1ML12_3MMAvBM", "IP_SA_P1M1ML12_3MMAvBM"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Real GDP growth trend versus benchmark currency",
"Industrial production trend versus benchmark currency",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Relative growth trends across currency areas",
)
Similarly, the relative employment growth and the relative unemployment gaps (vs respective benchmark) are displayed below:
xcatx = ["EMPL_NSA_P1M1ML12_3MMAvBM", "UNEMPLRATE_SA_3MMAv5YMANvBM"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Employment growth trend versus benchmark currency",
"Unemployment gap (negative) versus benchmark currency",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Relative labor market performance metrics across currency areas",
)
Panel znscoring #
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 zscores for a category panel around a specified neutral level (0 in our case). The term “znscore” refers to the normalized distance from the neutral value. We first define the categories that we want to calculate znscore, define min number of observations needed, the neutral value, frequency of reestimation and threshhold (4 times of standard deviation). The latter allows to exclude particular high volatility periods.
rafs = [
"BXBGDPRATIO_NSA_12MMA", # Basic external balance as % of GDP
"CABGDPRATIO_NSA_12MMA", # External current account as % of GDP
"MTBGDPRATIO_SA_3MMAv60MMA", # Merchandise trade balance (sa) as % of GDP: latest 3 months versus 5year average
"NIIPGDP_NSA", # Net international investment position as % of GDP
]
pafs_vbm = [paf + "vBM" for paf in pafs]
crrs = [
"FXCRR_NSABL5DM", # 5day median of FXCRR_NSA (Nominal forwardimplied carry vs. dominant cross: % ar)
"FXCRR_VT10BL5DM", # 5day median of FXCRR_VT10 (same as above, but % ar for 10% vol target)
"FXCRRHvGDRB_NSABL5DM", # 5day median of FXCRRHvGDRB_NSA (Nominal carry on 1month FX forward position, hedged against market directional risk.
]
acats = pafs_vbm + rafs + crrs
for cat in acats:
dfa = msp.make_zn_scores(
dfx,
xcat=cat,
cids=cids,
min_obs=3 * 261,
neutral="zero",
thresh=4,
est_freq="m",
)
dfx = msm.update_df(dfx, dfa)
ZNscore differences #
For carry modification one can use outright quantamental znscores or differences.

Modification by outright scores means that the absolute quantamental score modifies the carry. Thus relatively strong economic performance would enhance positive carry, whether or not that carry is already high or not.

Modification by relative score means that the difference between the quantamental and carry score modifies the carry. Thus, for positive carry only relatively strong economic performance that is even larger in score than the carry enhances it. Only modest relative economic scores would actually reduce it.
Here the focus is on znscore differences so far.
dict_afz = {
"BXB": "BXBGDPRATIO_NSA_12MMAZN",
"CAB": "CABGDPRATIO_NSA_12MMAZN",
"MTB": "MTBGDPRATIO_SA_3MMAv60MMAZN",
"NIP": "NIIPGDP_NSAZN",
"GDPvBM": "INTRGDP_NSA_P1M1ML12_3MMAvBMZN",
"INPvBM": "IP_SA_P1M1ML12_3MMAvBM",
"EMPvBM": "EMPL_NSA_P1M1ML12_3MMAvBM",
"UMNvBM": "UNEMPLRATE_SA_3MMAv5YMANvBM",
}
dict_crz = {
"CRR": "FXCRR_NSABL5DMZN",
"CRV": "FXCRR_VT10BL5DMZN",
"CRH": "FXCRRHvGDRB_NSABL5DMZN",
}
calcs = []
for cr, crz in dict_crz.items():
for af, afz in dict_afz.items():
calcs += [f"{af}v{cr}_ZND = {afz}  {crz}"]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
znds = list(dfa["xcat"].unique())
ZNscore combinations #
For znscore combinations, we first calculate average znscores across logical group members and then reznscore them. Finally, we combine them with carry. We average coefficients groupwise across the available group members so as to not lose history due to a single or just a few shortmemory categories.
dict_acs = {
"GROWTH": ["IP_SA_P1M1ML12_3MMAvBMZN", "INTRGDP_NSA_P1M1ML12_3MMAvBMZN"],
"LABOR": ["EMPL_NSA_P1M1ML12_3MMAvBMZN", "UNEMPLRATE_SA_3MMAv5YMANvBMZN"],
"ECO": ["GROWTH", "LABOR"],
"XBAL": ["CABGDPRATIO_NSA_12MMAZN", "BXBGDPRATIO_NSA_12MMAZN"],
"XVUL": ["XBAL", "MTBGDPRATIO_SA_3MMAv60MMAZN", "NIIPGDP_NSAZN"],
"ECXV": ["ECO", "XVUL"],
}
for key, value in dict_acs.items():
dfxx = dfx[dfx["xcat"].isin(value)]
dfxx = dfxx.drop(columns=["xcat"])
dfa = dfxx.groupby(by=["cid", "real_date"]).mean().reset_index()
dfa["xcat"] = key
dfx = msm.update_df(dfx, dfa)
for cat in dict_acs.keys():
dfa = msp.make_zn_scores(
dfx,
xcat=cat,
cids=cids,
min_obs=3 * 261,
neutral="zero",
thresh=4,
est_freq="m",
)
dfx = msm.update_df(dfx, dfa)
calcs = []
for cr, crz in dict_crz.items():
for ac in dict_acs.keys():
calcs += [f"{cr}_{ac}_ZNC = {crz} + {ac}ZN"]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids)
dfx = msm.update_df(dfx, dfa)
zncs = list(dfa["xcat"].unique())
Here we compare the voltargeted real FX carry signal (znscored) and the same signal balanced by relative growth and labor market performance.
xcatx = ["FXCRR_VT10BL5DMZN", "CRV_ECO_ZNC"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Voltargeted real FX carry signal",
"balanced by relative growth and labor market performance",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Voladjusted carry: outright signal and balanced signal",
)
Similar analysis for hedged against market directional risk carry displays the relative performance of the balanced signal versus the outright signal for hedged carry. Balancing affects both the magnitude and direction of the carry signal.
xcatx = ["FXCRRHvGDRB_NSABL5DMZN", "CRH_ECO_ZNC"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Hedged real FX carry signal",
"balanced by relative growth and labor market performance",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Hedged carry: outright signal and balanced signal",
)
Sigmoid modification #
Sigmoid function #
The basis for modifying realcarry signals is a logistic (“sigmoid”) transformation of differences of the zscores of performance and risk factors. This transformation results in values between 0 and 2 for the adjustment factor. The goal is to modify the realcarry signal by considering the difference between economic performance and carry, where a positive real carry should be enhanced by strong economic performance and diminished by weak relative performance, and vice versa for negative carry. The economic performance should not dominate the signal, but only modify its magnitude.
Here is a breakdown of the formula for adjusted carry:

Calculate the coefficient (coef) for adjustment:
coef = 2 / (1 + exp(  sign(crr) * znr ))
crr represents the real carry signal, and znr is the zscore of the economic factors.

Define the adjusted carry (cra) based on the sign of the real carry:
For positive carry: cra = coef * crr For negative carry: cra = (2  coef) * crr

Or in one equation:
cra = ((1  sign(crr)) + sign(crr) * coef )* crr
The formula ensures that the adjustment coefficient is between 0 and 2, depending on the sign of the real carry. A strong positive economic performance enhances the positive carry signal, while a weak relative performance diminishes it. Similarly, for negative carry, a weak economic performance enhances the negative signal, and a strong performance diminishes it.
The sigmoid function has been specified such that a 2 SD difference between the carry score and the performance or risk score reduces the carry signal by 75%. Similarly, a 2 SD difference between the performance or risk scores and the carry scores increases the carry signal by 75%.
This type of adjustment only manages the carry signal. It never supersedes it. By incorporating the adjustment factor based on economic performance, you modify the magnitude of the realcarry signal while still considering its sign. This allows you to balance the impact of economic performance on the carry metric without overwhelming its influence, resulting in an adjusted carry that reflects both the carry itself and the relative economic performance.
It’s worth noting that the formula can be customized further based on specific requirements, such as adjusting the coefficient range or introducing additional parameters. Adjustments and finetuning can be made based on analysis objectives and the characteristics of the data and factors.
def sigmoid(x):
return 2 / (1 + np.exp(1 * x))
ar = np.array([i for i in range(8, 9)])
plt.figure(figsize=(8, 6), dpi=80)
plt.plot(ar, sigmoid(ar))
plt.title(
"Logistic function that translates zscore difference into modification coefficient"
)
plt.show()
Sigmoid coefficients #
Calculate sigmoid functions of znscore differences between adjustment factor and carry.
calcs = []
for znd in znds:
calcs += [f"{znd}_C = ( {znd} ).applymap( lambda x: 2 / (1 + np.exp(  x)) ) "]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
afs = list(dfa["xcat"].unique())
Averaging coefficients groupwise across available group members is a useful approach to avoid losing historical information due to missing signals or a few categories with short memory. This approach allows to incorporate the contributions of multiple group members, ensuring a more comprehensive representation of the data.
dict_acz = {}
for cr in dict_crz.keys():
dict_add = {
f"GROWTHv{cr}_C": [f"GDPvBMv{cr}_ZND_C", f"INPvBMv{cr}_ZND_C"],
f"LABORv{cr}_C": [f"EMPvBMv{cr}_ZND_C", f"UMNvBMv{cr}_ZND_C"],
f"ECOv{cr}_C": [f"GROWTHv{cr}_C", f"LABORv{cr}_C"],
f"XBALv{cr}_C": [f"CABv{cr}_ZND_C", f"BXBv{cr}_ZND_C"],
f"XVULv{cr}_C": [f"XBALv{cr}_C", f"MTBv{cr}_ZND_C", f"NIPv{cr}_ZND_C"],
f"ECXVv{cr}_C": [f"ECOv{cr}_C", f"XVULv{cr}_C"],
}
dict_acz.update(dict_add)
for key, value in dict_acz.items():
dfxx = dfx[dfx["xcat"].isin(value)]
dfxx = dfxx.drop(columns=["xcat"])
dfa = dfxx.groupby(by=["cid", "real_date"]).mean().reset_index()
dfa["xcat"] = key
dfx = msm.update_df(dfx, dfa)
For easier viewing of results, we combine the modification coefficients for growth and labor market performance and for overall economic performance. Combining here means averaging the coefficients, except for periods where only one coefficient could be calculated. Below chart displays coefficients based on growth differentials and on labor market differentials.
xcatx = ["GROWTHvCRR_C", "LABORvCRR_C"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"based on growth differentials",
"based on labor market differentials",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=True,
size=(12, 12),
all_xticks=True,
title="Real carry modification coefficients (excluding periods of market dysfunction)",
)
Carry adjustment #
The cell below adjusts the carry signals by the modification coefficients.
calcs = []
for cr, crz in dict_crz.items():
calcs += [f"{cr}_SIGN = np.sign( {crz} )"]
for af in dict_acs.keys():
sign = f"{cr}_SIGN"
coef = f"{af}v{cr}_C"
calcs += [f"{cr}m{af} = ( ( 1  {sign} ) + {sign} * {coef} ) * {crz}"]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
Performanceadjusted carry is a more stable, less volatile, series than standard carry as large deviation between carry and fundamentals are deemphasized. In particular, extreme carry values lead to adjusted values close to zero and are thus taken out as signals.
modcrs = [cr + "m" + af for cr in dict_crz.keys() for af in dict_acs.keys()]
[cr + "m" + af for cr in dict_crz.keys() for af in dict_acs.keys()]
['CRRmGROWTH',
'CRRmLABOR',
'CRRmECO',
'CRRmXBAL',
'CRRmXVUL',
'CRRmECXV',
'CRVmGROWTH',
'CRVmLABOR',
'CRVmECO',
'CRVmXBAL',
'CRVmXVUL',
'CRVmECXV',
'CRHmGROWTH',
'CRHmLABOR',
'CRHmECO',
'CRHmXBAL',
'CRHmXVUL',
'CRHmECXV']
xcatx = ["FXCRR_NSABL5DMZN", "CRRmECO"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Voltargeted real FX carry signal",
"modified by relative growth and labor market performance",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Voladjusted carry: outright signal and modified signal",
)
The same visualization is done for hedged real carry strategy, which takes both FX forward and hedge basket positions for trades across all tradable FX forwards markets at a monthly frequency. As for voltargeted carry, modification changes mainly relative signal strength, but also seems to enhance signal stability, which suggests that it will probably save some transaction costs (transaction costs are not explicitly considered in this analysis). Hedged carry alone has been more prone to outliers due to instability in estimated hedge ratios.
xcatx = ["FXCRRHvGDRB_NSABL5DMZN", "CRHmECO"]
msp.view_timelines(
dfx,
xcats=xcatx,
xcat_labels=[
"Voltargeted real FX carry signal",
"modified by relative growth and labor market performance",
],
cids=cids_fxx,
ncol=4,
cumsum=False,
start="20000101",
same_y=False,
size=(12, 12),
all_xticks=True,
title="Hedged carry: outright signal and modified signal",
)
Value checks #
Modified carry #
Modified voltargeted carry #
Here we check and compare simple trading strategies based on the value generated by the modified carry signal and the hedged carry signal. In preparation for simple PnL calculation, we define the target variable (
FXXR_VT10
) and the main signal (
CRVmECXV
 voltargeted real FX carry signal modified by relative growth and labor market performance). As alternative signals we consider FX carries modified with different economic indicators calculated earlier in the notebook.
dict_mcrv = {
"sig": "CRVmECXV",
"rivs": [
"CRVmGROWTH",
"CRVmLABOR",
"CRVmECO",
"CRVmXBAL",
"CRVmXVUL",
"FXCRR_VT10BL5DMZN",
],
"targ": "FXXR_VT10",
"cidx": cids_fxx,
"srr": None,
"pnls": None,
}
dix = dict_mcrv
sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=[sig] + rivs,
rets=targ,
freqs="M",
start="20000101",
blacklist=fxblack,
)
dict_mcrv["srr"] = srr
dix = dict_mcrv
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  auc  

Panel  0.548  0.536  0.735  0.542  0.561  0.511  0.056  0.000  0.051  0.000  0.528 
Mean years  0.550  0.538  0.728  0.538  0.561  0.515  0.061  0.361  0.053  0.324  0.531 
Positive ratio  0.792  0.833  0.958  0.667  0.708  0.542  0.750  0.542  0.833  0.625  0.833 
Mean cids  0.548  0.535  0.739  0.545  0.556  0.514  0.036  0.440  0.037  0.411  0.517 
Positive ratio  0.808  0.769  0.923  0.846  0.808  0.538  0.654  0.346  0.769  0.500  0.769 
dix = dict_mcrv
srrx = dix["srr"]
display(srrx.signals_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

CRVmECXV  0.548  0.536  0.735  0.542  0.561  0.511  0.056  0.000  0.051  0.000  0.528 
CRVmGROWTH  0.548  0.536  0.735  0.542  0.561  0.511  0.063  0.000  0.053  0.000  0.528 
CRVmLABOR  0.547  0.536  0.734  0.541  0.560  0.511  0.071  0.000  0.057  0.000  0.528 
CRVmECO  0.548  0.536  0.735  0.542  0.561  0.511  0.075  0.000  0.061  0.000  0.528 
CRVmXBAL  0.548  0.536  0.735  0.542  0.562  0.511  0.013  0.308  0.027  0.001  0.528 
CRVmXVUL  0.548  0.536  0.735  0.542  0.561  0.511  0.010  0.406  0.025  0.003  0.528 
FXCRR_VT10BL5DMZN  0.548  0.536  0.735  0.542  0.561  0.511  0.040  0.001  0.044  0.000  0.528 
dix = dict_mcrv
sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
naive_pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigx,
cids=cidx,
start="20000101",
blacklist=fxblack,
# bms="EUR_FXXR_NSA",
)
for sig in sigx:
naive_pnl.make_pnl(
sig,
sig_neg=False,
sig_op="zn_score_pan",
thresh=4,
rebal_freq="monthly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "_PZN",
)
dict_mcrv["pnls"] = naive_pnl
The plot below compares the performance of the naive carry strategy to the performance of the modified carry strategy. The modified strategy is the naive strategy with the carry signal modified by relative growth and labor market performance. The modified strategy is more profitable than the simple voltargeted carry strategy on its own and modified with economic indicators (growth, labor market and both growth and labor at the same time)
dix = dict_mcrv
sigx = [
"FXCRR_VT10BL5DMZN",
"CRVmGROWTH",
"CRVmLABOR",
"CRVmECO",
]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
dict_labels={"FXCRR_VT10BL5DMZN_PZN":"voltargeted real carry signal",
"CRVmGROWTH_PZN": "modified by relative growth performance",
"CRVmLABOR_PZN": "modified by relative labor market performance",
"CRVmECO_PZN": "modified by both"
}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
title="Simple and modified voltargeted carry strategies PnLs (10% vol scale, 26 currencies)",
xcat_labels=dict_labels,
figsize=(16, 8),
)
Simulated PnLs of modified voladjusted carry strategies are substantially better not only on absolute basis, but also in terms of Sharpe and Sortino ratios: Both ratios increase substantially from 0.2 and 0.3 respectively without modification to 0.6 and 0.91 using the average of growth and labour market modification. This means volatilityadjusted returns have more than doubled thanks to modification. For labour market modification alone the Sharpe ratio would have been just below 0.7.
dix = dict_mcrv
sigx = [
"CRVmGROWTH",
"CRVmLABOR",
"CRVmECO",
"FXCRR_VT10BL5DMZN",
] # [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="20000101",
)
display(df_eval)
xcat  CRVmECO_PZN  CRVmGROWTH_PZN  CRVmLABOR_PZN  FXCRR_VT10BL5DMZN_PZN 

Return (pct ar)  8.646821  7.731052  8.777746  5.588816 
St. Dev. (pct ar)  10.0  10.0  10.0  10.0 
Sharpe Ratio  0.864682  0.773105  0.877775  0.558882 
Sortino Ratio  1.20344  1.075685  1.224797  0.77533 
Max 21day draw  19.804979  20.332016  23.344979  19.908141 
Max 6month draw  36.018961  35.031022  32.786924  31.041654 
Traded Months  280  280  280  280 
Modified real hedged carry #
Here we perform similar analysis for the hedged carry strategy. The target variable is Hedged FX forward return, the main signal is hedged carry modified by relative growth, labor market, external ratios, external balance ratios, and international investment position. The rival signals are the same as for the voladjusted carry strategy, but calculated on hedged positions.
dict_mcrh = {
"sig": "CRHmECXV",
"rivs": [
"CRHmGROWTH",
"CRHmLABOR",
"CRHmECO",
"CRHmXBAL",
"CRHmXVUL",
"FXCRRHvGDRB_NSABL5DMZN",
],
"targ": "FXXRHvGDRB_NSA",
"cidx": cids_fxx,
"srr": None,
"pnls": None,
}
dix = dict_mcrh
sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=[sig] + rivs,
rets=targ,
freqs="M",
start="20000101",
blacklist=fxblack,
)
dict_mcrh["srr"] = srr
dix = dict_mcrh
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  auc  

Panel  0.543  0.546  0.396  0.502  0.557  0.534  0.083  0.000  0.071  0.000  0.544 
Mean years  0.547  0.546  0.403  0.501  0.556  0.536  0.079  0.285  0.063  0.235  0.541 
Positive ratio  0.917  0.875  0.250  0.583  0.792  0.708  0.917  0.750  0.917  0.833  0.875 
Mean cids  0.543  0.546  0.404  0.503  0.554  0.538  0.074  0.263  0.069  0.230  0.537 
Positive ratio  0.962  0.923  0.269  0.423  0.846  0.808  0.885  0.769  0.962  0.769  0.923 
dix = dict_mcrh
srrx = dix["srr"]
display(srrx.signals_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

CRHmECXV  0.543  0.546  0.396  0.502  0.557  0.534  0.083  0.0  0.071  0.0  0.544 
CRHmGROWTH  0.543  0.546  0.396  0.502  0.557  0.534  0.073  0.0  0.071  0.0  0.544 
CRHmLABOR  0.542  0.544  0.393  0.500  0.554  0.534  0.084  0.0  0.075  0.0  0.542 
CRHmECO  0.543  0.546  0.396  0.502  0.557  0.534  0.086  0.0  0.077  0.0  0.544 
CRHmXBAL  0.544  0.546  0.395  0.502  0.558  0.534  0.059  0.0  0.057  0.0  0.544 
CRHmXVUL  0.543  0.546  0.396  0.502  0.557  0.534  0.064  0.0  0.057  0.0  0.544 
FXCRRHvGDRB_NSABL5DMZN  0.543  0.546  0.396  0.502  0.557  0.534  0.076  0.0  0.066  0.0  0.544 
dix = dict_mcrh
sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
naive_pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigx,
cids=cidx,
start="20000101",
blacklist=fxblack,
# bms="EUR_FXXR_NSA",
)
for sig in sigx:
naive_pnl.make_pnl(
sig,
sig_neg=False,
sig_op="zn_score_pan",
thresh=4,
rebal_freq="monthly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "_PZN",
)
dict_mcrh["pnls"] = naive_pnl
dix = dict_mcrh
sigx = [
"CRHmGROWTH",
"CRHmLABOR",
"CRHmECO",
"FXCRRHvGDRB_NSABL5DMZN",
] # [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="20000101",
)
display(df_eval)
xcat  CRHmECO_PZN  CRHmGROWTH_PZN  CRHmLABOR_PZN  FXCRRHvGDRB_NSABL5DMZN_PZN 

Return (pct ar)  9.274164  9.111688  9.217159  8.507911 
St. Dev. (pct ar)  10.0  10.0  10.0  10.0 
Sharpe Ratio  0.927416  0.911169  0.921716  0.850791 
Sortino Ratio  1.324127  1.306364  1.32104  1.230493 
Max 21day draw  20.440585  19.902178  20.76419  14.509827 
Max 6month draw  33.235589  30.832945  29.141803  17.653845 
Traded Months  280  280  280  280 
dix = dict_mcrh
sigx = [
"FXCRRHvGDRB_NSABL5DMZN",
"CRHmGROWTH",
"CRHmLABOR",
"CRHmECO",
] # [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
dict_labels={"FXCRRHvGDRB_NSABL5DMZN_PZN":"hedged real carry signal",
"CRHmGROWTH_PZN": "modified by relative growth performance",
"CRHmLABOR_PZN": "modified by relative labor market performance",
"CRHmECO_PZN": "modified by both"
}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
title="PnL: Economicallyenhanced hedged carry strategy (10% vol scale, 26 currencies)",
xcat_labels=dict_labels,
figsize=(15, 8),
)
Balanced carry #
Balanced voladjusted carry #
dict_bcrv = {
"sig": "CRV_ECXV_ZNC",
"rivs": [
"CRV_GROWTH_ZNC",
"CRV_LABOR_ZNC",
"CRV_ECO_ZNC",
"CRV_XBAL_ZNC",
"CRV_XVUL_ZNC",
"FXCRR_VT10BL5DMZN",
],
"targ": "FXXR_VT10",
"cidx": cids_fxx,
"srr": None,
"pnls": None,
}
dix = dict_bcrv
sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=[sig] + rivs,
rets=targ,
freqs="M",
start="20000101",
blacklist=fxblack,
)
dict_bcrv["srr"] = srr
dix = dict_bcrv
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  auc  

Panel  0.540  0.528  0.694  0.542  0.560  0.497  0.067  0.000  0.053  0.000  0.524 
Mean years  0.541  0.521  0.689  0.538  0.550  0.493  0.052  0.385  0.042  0.374  0.516 
Positive ratio  0.708  0.667  0.917  0.667  0.667  0.417  0.750  0.542  0.833  0.625  0.667 
Mean cids  0.540  0.531  0.706  0.545  0.562  0.478  0.068  0.288  0.057  0.243  0.521 
Positive ratio  0.885  0.692  0.808  0.846  0.846  0.462  0.808  0.692  0.846  0.731  0.654 
dix = dict_bcrv
sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
naive_pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigx,
cids=cidx,
start="20000101",
blacklist=fxblack,
# bms="EUR_FXXR_NSA",
)
for sig in sigx:
naive_pnl.make_pnl(
sig,
sig_neg=False,
sig_op="zn_score_pan",
thresh=4,
rebal_freq="monthly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "_PZN",
)
dict_bcrv["pnls"] = naive_pnl
dix = dict_bcrv
sigx = [
"FXCRR_VT10BL5DMZN",
"CRV_GROWTH_ZNC",
"CRV_LABOR_ZNC",
"CRV_ECO_ZNC",
]
pnls = [sig + "_PZN" for sig in sigx]
dict_labels={"FXCRR_VT10BL5DMZN_PZN":"voltargeted real carry signal",
"CRV_GROWTH_ZNC_PZN": "balanced with relative growth performance",
"CRV_LABOR_ZNC_PZN": "balanced with relative labor market performance",
"CRV_ECO_ZNC_PZN": "balanced with both"
}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
title="Simple and balanced voltargeted carry strategies PnLs (10% vol scale, 26 currencies)",
xcat_labels=dict_labels,
figsize=(16, 8),
)
dix = dict_bcrv
sigx = [
"FXCRR_VT10BL5DMZN",
"CRV_GROWTH_ZNC",
"CRV_LABOR_ZNC",
"CRV_ECO_ZNC",
] # [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="20000101",
)
display(df_eval)
xcat  CRV_ECO_ZNC_PZN  CRV_GROWTH_ZNC_PZN  CRV_LABOR_ZNC_PZN  FXCRR_VT10BL5DMZN_PZN 

Return (pct ar)  10.793394  8.207734  11.103256  5.588816 
St. Dev. (pct ar)  10.0  10.0  10.0  10.0 
Sharpe Ratio  1.079339  0.820773  1.110326  0.558882 
Sortino Ratio  1.539895  1.151135  1.604338  0.77533 
Max 21day draw  23.418182  19.811124  22.854966  19.908141 
Max 6month draw  38.765039  34.29457  35.527417  31.041654 
Traded Months  280  280  280  280 
Balanced hedged carry #
The cells below compare original simple carry strategy with the balanced carry strategy. As before, we balance the original carry with economic indicators.
dict_bcrh = {
"sig": "CRH_ECXV_ZNC",
"rivs": [
"CRH_GROWTH_ZNC",
"CRH_LABOR_ZNC",
"CRH_ECO_ZNC",
"CRH_XBAL_ZNC",
"CRH_XVUL_ZNC",
"FXCRRHvGDRB_NSABL5DMZN",
],
"targ": "FXXRHvGDRB_NSA",
"cidx": cids_fxx,
"srr": None,
"pnls": None,
}
dix = dict_bcrh
sig = dix["sig"]
rivs = dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=[sig] + rivs,
rets=targ,
freqs="M",
start="20000101",
blacklist=fxblack,
)
dict_bcrh["srr"] = srr
dix = dict_bcrh
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  auc  

Panel  0.532  0.532  0.508  0.502  0.534  0.531  0.078  0.000  0.064  0.000  0.532 
Mean years  0.534  0.525  0.506  0.501  0.527  0.524  0.069  0.349  0.053  0.349  0.525 
Positive ratio  0.875  0.708  0.458  0.583  0.667  0.625  0.792  0.583  0.875  0.667  0.708 
Mean cids  0.530  0.549  0.518  0.503  0.544  0.554  0.080  0.207  0.071  0.223  0.530 
Positive ratio  0.654  0.808  0.500  0.423  0.731  0.808  0.885  0.731  0.885  0.731  0.808 
dix = dict_bcrh
srrx = dix["srr"]
display(srrx.signals_table().astype("float").round(3))
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

CRH_ECXV_ZNC  0.532  0.532  0.508  0.502  0.534  0.531  0.078  0.0  0.064  0.0  0.532 
CRH_GROWTH_ZNC  0.538  0.538  0.529  0.502  0.538  0.539  0.072  0.0  0.066  0.0  0.538 
CRH_LABOR_ZNC  0.540  0.540  0.471  0.500  0.543  0.537  0.084  0.0  0.079  0.0  0.540 
CRH_ECO_ZNC  0.541  0.541  0.516  0.502  0.542  0.541  0.087  0.0  0.081  0.0  0.541 
CRH_XBAL_ZNC  0.524  0.524  0.503  0.502  0.526  0.521  0.057  0.0  0.047  0.0  0.524 
CRH_XVUL_ZNC  0.514  0.515  0.406  0.502  0.520  0.510  0.053  0.0  0.040  0.0  0.514 
FXCRRHvGDRB_NSABL5DMZN  0.543  0.546  0.396  0.502  0.557  0.534  0.076  0.0  0.066  0.0  0.544 
dix = dict_bcrh
sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
naive_pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigx,
cids=cidx,
start="20000101",
blacklist=fxblack,
# bms="USD_EQXR_NSA",
)
for sig in sigx:
naive_pnl.make_pnl(
sig,
sig_neg=False,
sig_op="zn_score_pan",
thresh=4,
rebal_freq="monthly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "_PZN",
)
dict_bcrh["pnls"] = naive_pnl
Balancing hedged carry leads to a slight increase in monthly accuracy for an average of the growth and labor market score.
dix = dict_bcrh
sigx = [
"FXCRRHvGDRB_NSABL5DMZN",
"CRH_GROWTH_ZNC",
"CRH_LABOR_ZNC",
"CRH_ECO_ZNC",
] # [dix["sig"]] + dix["rivs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
dict_labels={"FXCRRHvGDRB_NSABL5DMZN_PZN":"voltargeted real carry signal",
"CRH_GROWTH_ZNC_PZN": "balanced with relative growth performance",
"CRH_LABOR_ZNC_PZN": "balanced with relative labor market performance",
"CRH_ECO_ZNC_PZN": "balanced with both"
}
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
title="Simple and balanced real hedged carry strategies' PnLs (scalled to 10% ar vol)",
xcat_labels=dict_labels,
figsize=(16, 8),
)
dix = dict_bcrh
sigx = [
"CRH_GROWTH_ZNC",
"CRH_LABOR_ZNC",
"CRH_ECO_ZNC",
"FXCRRHvGDRB_NSABL5DMZN",
] # [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="20000101",
)
display(df_eval)
xcat  CRH_ECO_ZNC_PZN  CRH_GROWTH_ZNC_PZN  CRH_LABOR_ZNC_PZN  FXCRRHvGDRB_NSABL5DMZN_PZN 

Return (pct ar)  8.150934  7.535211  8.075363  8.507911 
St. Dev. (pct ar)  10.0  10.0  10.0  10.0 
Sharpe Ratio  0.815093  0.753521  0.807536  0.850791 
Sortino Ratio  1.161224  1.076306  1.152726  1.230493 
Max 21day draw  27.3179  20.471036  27.027449  14.509827 
Max 6month draw  40.849852  34.747645  37.893412  17.653845 
Traded Months  280  280  280  280 