# Advanced FX carry strategies with valuation adjustment

## Contents

# Advanced FX carry strategies with valuation adjustment#

This notebook illustrates of the points discussed in the post “Advanced FX carry strategies with valuation adjustment” available on the Macrosynergy website.

FX forward-implied carry is a popular ingredient in currency trading strategies because it is related to risk premia and implicit policy subsidies. Its signal value can often be increased by considering inflation differentials, hedging costs, data outliers, and market restrictions. However, even then, FX carry is a very imprecise guide for performance, and previous research has shown the benefits of enhancements based on economic performance (view post here). This post analyses the adjustment of real carry measures by currency over- or undervaluation. As a reference point, it uses point-in-time metrics of purchasing power parity-based valuation estimates that are partly or fully adjusted for historical gaps. The adjustment is conceptually compelling and has historically increased the value of carry signals across a variety of FX carry strategies.

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

The notebook covers the three main parts:

Get Packages and JPMaQS Data: This section is responsible for installing and importing the necessary Python packages used throughout the analysis.

Transformations and Checks: In this part, the notebook performs calculations and transformations on the data to derive the relevant signals and targets used for the analysis, including rolling median calculation, relative values, or building simple linear composite indicators.

Value Checks: This is the most critical section, where the notebook calculates and implements the trading strategies based on the hypotheses tested in the post. This section involves backtesting simple trading strategies. In particular, the post investigates the use of directional (relative) adjusted real carry for outright (relative) FX forward returns trading. The last part looks at the example of use of hedged carry.

It is important to note that while the notebook covers a selection of indicators and strategies used for the post’s main findings, users can explore countless other possible indicators and approaches. Users can modify the code to test different hypotheses and strategies based on their research and ideas. Best of luck with your research!

## Get packages and JPMaQS data#

```
# Run only if needed!
"""
%%capture
! pip install macrosynergy --upgrade"""
```

```
'\n%%capture\n! pip install macrosynergy --upgrade'
```

```
import numpy as np
import pandas as pd
from pandas import Timestamp
import matplotlib.pyplot as plt
import seaborn as sns
import os
from IPython.display import display, Markdown
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")
```

This notebook downloads selected indicators for the following cross-sections: AUD (Australian dollar), BRL (Brazilian real), CAD (Canadian dollar), CHF (Swiss franc), CLP (Chilean peso), COP (Colombian peso), CZK (Czech Republic koruna), EUR (euro), GBP (British pound), HUF (Hungarian forint), IDR (Indonesian rupiah), ILS (Israeli shekel), INR (Indian rupee), JPY (Japanese yen), KRW (Korean won), MXN (Mexican peso), MYR (Malaysian ringgit), NOK (Norwegian krone), NZD (New Zealand dollar), PEN (Peruvian sol), PHP(Philippine peso), PLN (Polish zloty), RON (Romanian leu), RUB (Russian ruble), SGD(Singapore dollar), SEK (Swedish krona), THB(Thai baht), TRY (Turkish lira), TWD (Taiwanese dollar), USD (U.S. dollar), ZAR (South African rand). For convenience purposes, the cross-sections are collected in a few lists, such as Developed markets currencies, emerging markets, regional cross-sections etc.

```
# General cross-sections lists
cids_g3 = ["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", "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", "TWD"] # 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
```

The description of each JPMaQS category is available under Macro quantamental academy, or JPMorgan Markets (password protected). For tickers used in this notebook, see FX forward carry, PPP exchange rates, FX tradeability and flexibility, and FX forward returns

```
# Categories
fxcr = [
"FXCRY_NSA",
"FXCRY_VT10",
"FXCRR_NSA",
"FXCRR_VT10",
"FXCRRHvGDRB_NSA",
]
povs = [
"PPPFXOVERVALUE_NSA"
]
main = fxcr + povs
rets = [
"FXTARGETED_NSA",
"FXUNTRADABLE_NSA",
"FXXR_NSA",
"FXXR_VT10",
"FXXRHvGDRB_NSA",
]
xcats = main + rets
xtix = ["USD_EQXR_NSA", "GLB_DRBXR_NSA"]
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 343
```

```
# Download series from J.P. Morgan DataQuery by tickers
start_date = "1990-01-01"
# Retrieve credentials
client_id: str = os.getenv("DQ_CLIENT_ID")
client_secret: str = os.getenv("DQ_CLIENT_SECRET")
# Download from DataQuery
with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as downloader:
df = downloader.download(
tickers=tickers,
start_date=start_date,
metrics=["value",],
suppress_warning=True,
show_progress=True,
)
```

```
Downloading data from JPMaQS.
Timestamp UTC: 2024-01-11 18:10:34
Connection successful!
Number of expressions requested: 343
```

```
Requesting data: 100%|██████████| 18/18 [00:05<00:00, 3.30it/s]
Downloading data: 100%|██████████| 18/18 [00:16<00:00, 1.06it/s]
```

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 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

```
dfx = df.copy()
dfx.info()
```

```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2206664 entries, 0 to 2206663
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: 67.3+ MB
```

### Blacklist dictionary for FX#

Before running the analysis, `make_blacklist()`

helper function from the `macrosynergy`

package, 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.

```
dfb = df[df["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
:, ["cid", "xcat", "real_date", "value"]
]
dfba = (
dfb.groupby(["cid", "real_date"])
.aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
.reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
```

```
{'BRL': (Timestamp('2012-12-03 00:00:00'), Timestamp('2013-09-30 00:00:00')),
'CHF': (Timestamp('2011-10-03 00:00:00'), Timestamp('2015-01-30 00:00:00')),
'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
'ILS': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-12-30 00:00:00')),
'INR': (Timestamp('1999-01-01 00:00:00'), Timestamp('2004-12-31 00:00:00')),
'MYR_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2007-11-30 00:00:00')),
'MYR_2': (Timestamp('2018-07-02 00:00:00'), Timestamp('2024-01-10 00:00:00')),
'PEN': (Timestamp('2021-07-01 00:00:00'), Timestamp('2021-07-30 00:00:00')),
'RON': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-11-30 00:00:00')),
'RUB_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2005-11-30 00:00:00')),
'RUB_2': (Timestamp('2022-02-01 00:00:00'), Timestamp('2024-01-10 00:00:00')),
'SGD': (Timestamp('1999-01-01 00:00:00'), Timestamp('2024-01-10 00:00:00')),
'THB': (Timestamp('2007-01-01 00:00:00'), Timestamp('2008-11-28 00:00:00')),
'TRY_1': (Timestamp('1999-01-01 00:00:00'), Timestamp('2003-09-30 00:00:00')),
'TRY_2': (Timestamp('2020-01-01 00:00:00'), Timestamp('2024-01-10 00:00:00'))}
```

### Availability#

It is important to assess data availability before conducting any analysis. It allows for the 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.

The function missing_in_df() displays (1) categories that are missing across all expected cross-sections for a given category name list, and (2) cross-sections that are missing within a category.

```
msm.missing_in_df(df, xcats=xcats, cids=cids)
```

```
Missing xcats across df: []
Missing cids for FXCRRHvGDRB_NSA: ['USD']
Missing cids for FXCRR_NSA: ['USD']
Missing cids for FXCRR_VT10: ['USD']
Missing cids for FXCRY_NSA: ['USD']
Missing cids for FXCRY_VT10: ['USD']
Missing cids for FXTARGETED_NSA: ['USD']
Missing cids for FXUNTRADABLE_NSA: ['USD']
Missing cids for FXXRHvGDRB_NSA: ['USD']
Missing cids for FXXR_NSA: ['USD']
Missing cids for FXXR_VT10: ['USD']
Missing cids for PPPFXOVERVALUE_NSA: ['USD', 'TWD']
```

check_availability() functions list visualizes start years and the number of missing values at or before the end date of all selected cross-sections and across a list of categories. It also displays unavailable indicators as gray fields and color codes for the starting year of each series, with darker colors indicating more recent starting years.

```
msm.check_availability(df, xcats=fxcr, cids=cids, missing_recent=False)
```

```
msm.check_availability(df, xcats=povs, cids=cids, missing_recent=False)
```

```
msm.check_availability(df, xcats=rets, cids=cids, missing_recent=False)
```

## Transformations and checks#

### Features#

#### FX forward-implied carry#

Using rolling medians can be a useful approach to mitigate the undue influence of short-term (untradable) carry distortions in financial data 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. 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 5-day rolling medians with postfix `_5DMM`

, which aligns with market conventions. This means that for each day, we calculate the median of the previous 5 days’ observations.

```
calcs = [f"{cr}_5DMM = ( {cr} ).rolling(5).median()" for cr in fxcr]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
```

JPMaQS records carry at the end of the local trading day. Sometimes, these records can be distorted by a single trade or reflect market conditions that compromise the information value of the data or do not allow trading at the recorded price. Therefore, we smooth all daily FX carry series in the form of a 5-day moving median (in the cell above) and contain the absolute value of real carry at 25%, positive or negative with the standard python `.clip()`

function, where all values below the minimum threshold (-25% in our case) are set to a threshold value; and all values above the threshold (25% in our case) will be set to it.

```
calcs = [f"{cr}_5DMMW25 = ( {cr}_5DMM ).clip(-25, 25)" for cr in fxcr]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
```

With the help of `view_ranges()`

and `view_timelines()`

from the `macrosynergy`

package we plot 5-day clipped moving averages of `FXCRR_NSA_5DMMW25`

(Real carry), and `FXCRRHvGDRB_NSA_5DMMW25`

(Real carry minus hedging cost).

```
xcatx = [
"FXCRR_NSA_5DMMW25",
"FXCRRHvGDRB_NSA_5DMMW25",
]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_ranges(dfx, xcatx, cids=cidx, start=sdate, size=(14, 7))
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Smoothed and winsorized real FX forward-implied carry types",
title_fontsize=20,
xcat_labels = ["Real carry", "Real carry minus hedging cost"],
)
```

#### PPP overvaluation#

##### Overvaluations to USD#

The basis for this notebook is PPP exchange rates: JPMaQS provides point-in-time information states of PPP exchange rates that combine annual official PPP exchange rate releases and CPI-based estimates of monthly changes up to the latest month for which CPIs have been released for both the local economy and the United States (view documentation here). Based on these estimated PPP exchange rates, JPMaQS also calculates PPP overvaluation ratios, i.e., ratios of the market-based USD value of the local currency and the PPP value.

We replace overvaluation ratio `PPPFXOVERVALUE_NSA`

with percentage change by subtracting 1 and multiplying the difference with 100.

```
calcs = ["PPPFXOVERVALUE_NSA_P = 100 * ( PPPFXOVERVALUE_NSA - 1)"]
cidx = cids_fx + ["EUR"]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cidx)
dfx = msm.update_df(dfx, dfa)
```

With the help of `view_ranges()`

and `view_timelines()`

from the `macrosynergy`

package we plot purchasing power parity-based over- or undervaluation versus USD, % of fair value.

```
xcatx = ["PPPFXOVERVALUE_NSA_P"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_ranges(
dfx,
xcatx,
cids=cidx,
start=sdate,
title="Purchasing power parity-based over- or undervaluation versus USD, % of fair value, ranges since 2000",
size=(14, 7),
kind="box",
)
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Overvaluation, % of fair value, versus USD",
title_fontsize=20,
)
```

#### Overvaluation to benchmark#

We calculate overvaluation percentages as % of the PPP value against the dominant benchmark currencies rather than the USD alone:

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

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

Currencies traded against USD: all other currencies in our dataset

Calculation is done with the help of `panel_calculator()`

function in `macrosynergy.panel`

. The resulting overvaluation to the dominant benchmark indicator will get postfix `_vBM`

```
xcatx = ["PPPFXOVERVALUE_NSA_P"]
for xc in xcatx:
calc_eur = [f"{xc}vBM = 100 * ( ( 1 + {xc} / 100 ) / ( 1 + iEUR_{xc} / 100 ) - 1 )"]
calc_usd = [f"{xc}vBM = {xc} "]
calc_eud = [
f"{xc}vBM = 0.5 * {xc} + 0.5 * 100 * ( ( 1 + {xc} / 100 ) / ( 1 + iEUR_{xc} / 100 ) - 1 )"
]
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)
```

With the help of `view_ranges()`

and `view_timelines()`

from the `macrosynergy`

package we plot purchasing power parity-based over- or undervaluation versus USD and vs natural benchmark, % of fair value.

```
xcatx = ["PPPFXOVERVALUE_NSA_P", "PPPFXOVERVALUE_NSA_PvBM"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_ranges(
dfx,
xcatx,
cids=cidx,
start=sdate,
title="Purchasing power parity-based over- or undervaluation, % of fair value, ranges since 2000",
xcat_labels=["versus USD", "versus natural benchmark (USD, EUR or both)"],
size=(14, 7),
kind="box",
)
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="PPP-based over- or undervaluation, % of fair value",
title_fontsize=20,
xcat_labels=["versus USD", "versus natural benchmark (USD, EUR or both)"],
)
```

To account for structural differences in purchasing power, we adjust overvaluation ratios for long-term median values. In particular, we calculate for each date a median overvaluation percentage since inception (typically early 1990s) up to that date and subtract this median from the concurrent overvaluation metric. We calculate both a full adjustment (`PPPFXOVERVALUE_NSA_PvBMvLTM`

) for the median and a partial (50%) adjustment (`PPPFXOVERVALUE_NSA_PvBMvSLTM`

) and consider both in subsequent analyses.

```
xcatx = ["PPPFXOVERVALUE_NSA_P", "PPPFXOVERVALUE_NSA_PvBM"]
calc_1 = [f"{xc}_LTM = {xc}.expanding(min_periods=21*24).median()" for xc in xcatx]
calc_2 = [f"{xc}vLTM = {xc} - {xc}_LTM" for xc in xcatx]
calc_3 = [f"{x}vSLTM = ( {x} + {x}vLTM ) / 2 " for x in xcatx]
calcs = calc_1 + calc_2 + calc_3
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx)
dfx = msm.update_df(dfx, dfa)
```

The outright overvaluation and the long-term trailing medians are displayed in the panel below using `view_timelines()`

from the `macrosynergy`

package

```
xcatx = ["PPPFXOVERVALUE_NSA_PvBM", "PPPFXOVERVALUE_NSA_PvBM_LTM"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Purchasing power parity-based overvaluation and long-term trailing median, % of fair value",
title_fontsize=20,
xcat_labels=["Overvaluation versus natural benchmark", "Long-term expanding sample median"],
)
```

```
xcatx = ["PPPFXOVERVALUE_NSA_PvBMvSLTM", "PPPFXOVERVALUE_NSA_PvBMvLTM"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Overvaluation versus long-term trailing median, % of fair value",
title_fontsize=20,
xcat_labels=["partial adjustment", "full adjustment"],
)
```

The cell below calculates the negatives for the overvaluation ratios and trends.

```
ovrts = [ # overvaluation ratios and trends
"PPPFXOVERVALUE_NSA_PvBMvLTM",
"PPPFXOVERVALUE_NSA_PvBMvSLTM",
]
xcatx = ovrts
calcs = [f"{xc}N = - {xc} " for xc in xcatx]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
```

#### Carry-overvaluation balance#

Based on plausible assumptions that 50% of overvaluation is expected to be correctly linearly over the coming 3-6 years, we subtract either one sixth of one twelfth of the overvaluation from the annualized real carry metrics. The respective carry indicators receive postfixes `_3A`

and `_6A`

```
fcrs = ["FXCRR_NSA_5DMMW25", "FXCRRHvGDRB_NSA_5DMMW25"]
dict_ovs = { # overvaluation metrics and plausible adjustment times in years
"PPPFXOVERVALUE_NSA_PvBMvLTM": (3, 6),
"PPPFXOVERVALUE_NSA_PvBMvSLTM": (3, 6),
}
calcs = []
for cr in fcrs:
for ov, at in dict_ovs.items():
croa = "_".join(cr.split("_")[:2] + [ov.split("_")[-1]])
for i in range(len(at)):
croa_new = croa + f"_{at[i]}A"
calcs.append(f"{croa_new} = {cr} - {ov} / {2 * at[i]}")
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids_fx, blacklist=fxblack)
dfx = msm.update_df(dfx, dfa)
crots = dfa['xcat'].unique().tolist()
crotx = crots + fcrs + [ov + "N" for ov in ovrts]
```

The resulting adjusted absolute real FX carry timelines are displayed below:

```
xcatx = [
"FXCRR_NSA_5DMMW25",
"FXCRR_NSA_PvBMvLTM_3A",
"FXCRR_NSA_PvBMvLTM_6A",
]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Smoothed real FX carry without, with full valuation adjustment (3-year and 6-year horizon)",
title_fontsize=20,
xcat_labels=["no adjustment", "full adjustment 3 years", "full adjustment 6 years"],
)
```

The below facet shows smoothed real FX and the effects of adjustments for a 3-year horizon.

```
xcatx = [
"FXCRR_NSA_5DMMW25",
"FXCRR_NSA_PvBMvLTM_3A",
"FXCRR_NSA_PvBMvSLTM_3A",
]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Smoothed real FX carry without, with full, and with partial overvaluation adjustment (3-year horizon)",
title_fontsize=20,
)
```

#### Relative features#

The convenience function `make_relative_value()`

of the `macrosynergy.panel`

module calculates values relative for all selected and derived carries and PPPs to an equally-weighted basket while adapting to missing periods of any of the basket cross sections. These relative signals receive postfix `vGFX`

. These relative signals will be later used for relative FX strategies.

```
xcatx = crotx + ovrts
cidx = cids_fx
sdate = "2000-01-01"
dfa = msp.make_relative_value(
dfx, xcats=xcatx, cids=cidx, start=sdate, blacklist=fxblack, postfix="vGFX"
)
dfx = msm.update_df(dfx, dfa)
```

The plot below compares absolute and relative adjusted carry side by side:

```
crot = "FXCRR_NSA_PvBMvLTM_3A"
xcatx = [crot, crot + "vGFX"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cidx,
ncol=5,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Absolute and relative adjusted carry",
title_fontsize=20,
)
```

### Targets#

#### Directional#

As directional targets we can consider three types of FX returns: outright cumulative FX forward return `FXXR_NSA`

, volatility targeted (for 10% vol target) dominant cross `FXXR_VT10`

and cumulative return on FX forward, hedged against market direction risk `FXXRHvGDRB_NSA`

.

```
xcatx = ["FXXR_NSA", "FXXR_VT10", "FXXRHvGDRB_NSA"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids_fx,
ncol=5,
cumsum=True,
start="2000-01-01",
same_y=False,
all_xticks=True,
title="Outright, and vol-targeted FX returns, and FX forward, hedged against market direction risk",
title_fontsize=20,
)
```

#### Relative targets#

The convenience function `make_relative_value()`

of the `macrosynergy.panel`

module calculates relative values for the three directional types of FX returns above. These values are calculated relative to an equally-weighted basket while adapting to missing periods of any of the basket cross sections. These relative returns receive postfix `vGFX`

(versus Global FX)

```
xcatx = ["FXXR_NSA", "FXXR_VT10", "FXXRHvGDRB_NSA"]
cidx = cids_fx
sdate = "2000-01-01"
dfa = msp.make_relative_value(
dfx, xcats=xcatx, cids=cidx, start=sdate, blacklist=fxblack, postfix="vGFX"
)
dfx = msm.update_df(dfx, dfa)
```

The facet below compares directional (`FXXR_VT10`

) with relative (`FXXR_VT10vGFX`

) FX returns (both for 10% vol target):

```
xcatx = ["FXXR_VT10", "FXXR_VT10vGFX"]
cidx = cids_fx
sdate = "2000-01-01"
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids_fx,
ncol=5,
cumsum=True,
start="2000-01-01",
same_y=False,
all_xticks=True,
title="Directional and relative vol-targeted FX returns",
title_fontsize=20,
)
```

## Value checks#

In this part of the analysis, the notebook calculates the naive PnLs (Profit and Loss) for financial returns (outright and relative FX forward returns in this notebook) using the previously derived FX carry indicators. The PnLs are calculated based on simple trading strategies that utilize the indicators as signals (no regression is involved). The strategies involve going long (buying) or short (selling) on returns based purely on the direction of the score signals.

To evaluate the performance of these strategies, the notebook computes various metrics and ratios, including:

Correlation: Measures the relationship between indicator changes and consequent financial returns. Positive correlations indicate that the strategy moves in the same direction as the market, while negative correlations indicate an opposite movement.

Accuracy Metrics: These metrics assess the accuracy of the confidence score-based strategies in predicting market movements. Standard accuracy metrics include accuracy rate, balanced accuracy, precision, etc.

Performance Ratios: Various performance ratios, such as Sharpe ratio, Sortino ratio, Max draws, etc.

The notebook compares the performance of the simple strategies based on real carry signals with the generic JPMaQS FX forward returns and with relative FX forward returns calculated earlier.

It’s important to note that the analysis deliberately disregards transaction costs and risk management considerations. This is done to provide a more straightforward comparison of the strategies’ raw performance without the additional complexity introduced by transaction costs and risk management, which can vary based on trading size, institutional rules, and regulations.

```
# Dictionary for chart labels
dict_labs = {
"FXCRR_NSA_PvBMvLTM_3A": "Real FX carry, full valuation adjustment over 3 years",
"FXCRR_NSA_PvBMvSLTM_3A": "Real FX carry, partial valuation adjustment over 3 years",
"FXCRR_NSA_PvBMvLTM_6A": "Real FX carry, full valuation adjustment over 6 years",
"FXCRR_NSA_PvBMvSLTM_6A": "Real FX carry, partial valuation adjustment over 6 years",
"FXCRR_NSA_5DMMW25": "Real FX carry, smoothed and winsorized",
"PPPFXOVERVALUE_NSA_PvBMvLTMN": "PPP-based overvaluation, partial adjustment",
"PPPFXOVERVALUE_NSA_PvBMvSLTMN": "PPP-based overvaluation, full adjustment",
}
```

### Directional adjusted real carry#

In this section, we specify signals and the target for the first hypothesis. Here, we test a simple idea that directional real carry can be used as a positive predictor of FX forward returns. Similarly, PPP-based overvaluation metrics have been significant (negative predictors), and hence, real carry metrics that have been adjusted for such overvaluation show a somewhat higher predictive correlation with returns than simple real carry.

#### Specs and panel test#

```
cr_type = "FXCRR_NSA"
feats = [cr for cr in crotx if cr_type in cr] + [ov + "N" for ov in ovrts]
targ = "FXXR_NSA"
cidx = cids_fx
start = "2000-01-01"
dict_crr = {
"sigs": feats,
"targ": targ,
"cidx": cidx,
"start": start,
"black": fxblack,
"srr": None,
"pnls": None,
}
```

`CategoryRelations()`

function is used for quick visualization and analysis of two categories, in particular, 5-day clipped real FX carry `FXCRR_NSA_5DMMW25`

and subsequent JPMaQS FX forward returns
. The `.reg_scatter()`

method is convenient for visualizing the relationship between two categories, including the strength of the linear association and any potential outliers. It includes a regression line with a 95% confidence interval, which can help assess the significance of the relationship. The analysis is done on quarterly basis.

```
dix = dict_crr
sig = 'FXCRR_NSA_5DMMW25'
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Real FX carry, smoothed and winsorized, quarter-end",
ylab="FX forward return, next quarter",
title="Real FX carry and subsequent FX forward returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

We use `CategoryRelations()`

function for quick visualization and analysis of two categories, in particular, overvaluation adjusted real FX carry, over 3 years `'FXCRR_NSA_PvBMvLTM_3A'`

and subsequent JPMaQS FX forward returns
. The `.reg_scatter()`

method visualizes the relationship between two categories, including the strength of the linear association and any potential outliers. It includes a regression line with a 95% confidence interval, which can help assess the significance of the relationship:

```
dix = dict_crr
sig = dix["sigs"][0]
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Valuation-adjusted real FX carry, smoothed and winsorized, quarter-end",
ylab="FX forward return, next quarter",
title="Valuation-adjusted real FX carry and subseq. FX returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

#### Accuracy and correlation check#

The `SignalReturnRelations`

class from the macrosynergy.signal module is specifically designed to analyze, visualize, and compare the relationships between panels of trading signals and panels of subsequent returns.

```
dix = dict_crr
sig = dix["sigs"][0]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=dix["sigs"],
sig_neg=[False, False, False, False, False, False, False],
rets=targ,
freqs="M",
start="2000-01-01",
blacklist=fxblack,
)
dix["srr"] = srr
dix = dict_crr
srrx = dix["srr"]
```

`multiple_relations_table()`

is a method that compares multiple signal-return relations in one table. It is useful to compare the performance of different signals against the same return series (more than one possible financial return) and multiple possible frequencies. The method returns a table with standard columns used for `single_relation_table()`

and other tables, but the rows display different signals from the list of signals specified upon SignalReturnsRelations () `sigs`

. The row names indicate the frequency (‘D,’ ‘W,’ ‘M,’ ‘Q,’ ‘A’) followed by the signal’s and return’s names.

```
display(srrx.multiple_relations_table().round(3))
```

#### Naive PnL#

`NaivePnl()`

class is designed to provide a quick and simple overview of a stylized PnL profile of a set of trading signals. The class carries the label naive because its methods do not consider transaction costs or position limitations, such as risk management considerations. This is deliberate because costs and limitations are specific to trading size, institutional rules, and regulations.

Important options within NaivePnl() function include:

`zn_score_pan`

option, which transforms raw signals into z-scores around zero value based on the whole panel. The neutral level & standard deviation will use the cross-section of panels. zn-score here means standardized score with zero being the neutral level and standardization through division by mean absolute value.rebalancing frequency (

`rebal_freq`

) for positions according to signal is chosen monthly,rebalancing slippage (

`rebal_slip`

) in days is 1, which means that it takes one day to rebalance the position and that the new position produces PnL from the second day after the signal has been recorded,threshold value (

`thresh`

) beyond which scores are winsorized, i.e., contained at that threshold. This is often realistic, as risk management and the potential of signal value distortions typically preclude outsized and concentrated positions within a strategy. We apply a threshold of 2.

The function below creates “PZN” PnL: `zn_score_pan`

(transforms raw signals into z-scores around zero value based on cross-section alone). We also create long only PnL, labeling it “Long only”

```
dix = dict_crr
sigx = dix["sigs"]
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", "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
```

The `plot_pnls()`

method of the `NaivePnl()`

class is used to plot a line chart of cumulative PnL

```
dix = dict_crr
start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx] + ["Long only"]
new_keys = [x + "_PZN" for x in dict_labs.keys()]
dict_labx = {new_key: dict_labs[old_key] for new_key, old_key in zip(new_keys, dict_labs)}
dict_labx["Long only"] = "Equally-weighted long-only portfolio "
labx = [dict_labx[x] for x in pnls]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Naive PnLs of simple valuation-adjusted FX carry strategies for all 25 currencies",
xcat_labels=labx,
figsize=(18, 10),
)
```

```
dix = dict_crr
start = dix["start"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="2000-01-01",
)
```

The method `evaluate_pnls()`

returns a small dataframe of key PnL statistics. For definitions of Sharpe and Sortino ratios, please see here

```
display(df_eval.transpose())
```

Return (pct ar) | St. Dev. (pct ar) | Sharpe Ratio | Sortino Ratio | Max 21-day draw | Max 6-month draw | USD_EQXR_NSA correl | EUR_FXXR_NSA correl | Traded Months | |
---|---|---|---|---|---|---|---|---|---|

xcat | |||||||||

FXCRR_NSA_5DMMW25_PZN | 4.782083 | 10.216752 | 0.468063 | 0.694725 | -20.50313 | -33.409496 | 0.293825 | 0.382854 | 289 |

FXCRR_NSA_PvBMvLTM_3A_PZN | 6.358297 | 10.217107 | 0.622319 | 0.959984 | -18.688547 | -25.008632 | 0.124834 | 0.024568 | 289 |

FXCRR_NSA_PvBMvLTM_6A_PZN | 6.18125 | 10.217446 | 0.60497 | 0.928656 | -17.065945 | -25.724367 | 0.239267 | 0.238762 | 289 |

FXCRR_NSA_PvBMvSLTM_3A_PZN | 5.832592 | 10.217182 | 0.570861 | 0.863706 | -19.042353 | -27.104034 | 0.290877 | 0.311782 | 289 |

FXCRR_NSA_PvBMvSLTM_6A_PZN | 5.4589 | 10.217035 | 0.534294 | 0.804172 | -17.643919 | -30.618635 | 0.299993 | 0.345846 | 289 |

### Relative adjusted real carry#

An alternative application of the adjusted carry signal is for trades across small-country currencies. We use relative real FX carry signals and return will be the FX forward position against benchmark currencies, vol-targeted to 10% annualized, against the 25-currency basket of these positions.

#### Specs and panel test#

Here we investigate predictive power of relative adjusted real carry on subsequent relative returns calculated earlier `FXXR_VT10vGFX`

```
cr_type = "FXCRR_NSA"
feats = [cr + "vGFX" for cr in crotx if cr_type in cr] + [ov + "NvGFX" for ov in ovrts]
targ = "FXXR_VT10vGFX"
cidx = cids_fx
start = "2000-01-01"
dict_crr_vgfx = {
"sigs": feats,
"targ": targ,
"cidx": cidx,
"start": start,
"black": fxblack,
"srr": None,
"pnls": None,
}
```

`CategoryRelations()`

function is used again for quick visualization and analysis of two categories, in particular, Relative real FX carry `'FXCRR_NSA_5DMMW25vGFX'`

and subsequent relative FX return `FXXR_VT10vGFX`

. The `.reg_scatter()`

method is convenient for visualizing the relationship between two categories, including the strength of the linear association and any potential outliers. It includes a regression line with a 95% confidence interval, which can help assess the significance of the relationship. The analysis is done on quarterly basis.

```
dix = dict_crr_vgfx
sig = 'FXCRR_NSA_5DMMW25vGFX'
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Real FX carry versus 25 currencies basket, smoothed and winsorized, quarter-end",
ylab="Vol-targeted FX forward return versus 25 currencies basket, next quarter",
title="Relative real FX carry and subsequent relative FX returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

```
dix = dict_crr_vgfx
sig = dix["sigs"][1]
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Valuation-adjusted real FX carry versus 25 currencies basket, smoothed and winsorized, quarter-end",
ylab="Vol-targeted FX forward return versus 25 currencies basket, next quarter",
title="Relative adj. real FX carry and subseq. relative FX returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

#### Accuracy and correlation check#

The `SignalReturnRelations`

class from the macrosynergy.signal module is specifically designed to analyze, visualize, and compare the relationships between panels of trading signals and panels of subsequent returns.

```
dix = dict_crr_vgfx
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=dix["sigs"],
sig_neg=[False, False, False, False, False, False, False],
rets=targ,
freqs="M",
start="2000-01-01",
blacklist=fxblack,
)
dix["srr"] = srr
dix = dict_crr_vgfx
srrx = dix["srr"]
```

```
display(srrx.multiple_relations_table().astype("float").round(3))
```

accuracy | bal_accuracy | pos_sigr | pos_retr | pos_prec | neg_prec | pearson | pearson_pval | kendall | kendall_pval | map_pval | auc | |
---|---|---|---|---|---|---|---|---|---|---|---|---|

M: FXCRR_NSA_5DMMW25vGFX/last => FXXR_VT10vGFX | 0.528 | 0.530 | 0.424 | 0.511 | 0.546 | 0.514 | 0.063 | 0.0 | 0.057 | 0.000 | 0.0 | 0.529 |

M: FXCRR_NSA_PvBMvLTM_3AvGFX/last => FXXR_VT10vGFX | 0.532 | 0.533 | 0.462 | 0.512 | 0.548 | 0.519 | 0.081 | 0.0 | 0.062 | 0.000 | 0.0 | 0.533 |

M: FXCRR_NSA_PvBMvLTM_6AvGFX/last => FXXR_VT10vGFX | 0.531 | 0.533 | 0.440 | 0.512 | 0.548 | 0.517 | 0.076 | 0.0 | 0.064 | 0.000 | 0.0 | 0.532 |

M: FXCRR_NSA_PvBMvSLTM_3AvGFX/last => FXXR_VT10vGFX | 0.538 | 0.538 | 0.516 | 0.512 | 0.549 | 0.528 | 0.087 | 0.0 | 0.068 | 0.000 | 0.0 | 0.538 |

M: FXCRR_NSA_PvBMvSLTM_6AvGFX/last => FXXR_VT10vGFX | 0.538 | 0.539 | 0.475 | 0.512 | 0.553 | 0.525 | 0.084 | 0.0 | 0.069 | 0.000 | 0.0 | 0.539 |

M: PPPFXOVERVALUE_NSA_PvBMvLTMNvGFX/last => FXXR_VT10vGFX | 0.510 | 0.509 | 0.524 | 0.511 | 0.520 | 0.498 | 0.046 | 0.0 | 0.022 | 0.007 | 0.0 | 0.509 |

M: PPPFXOVERVALUE_NSA_PvBMvSLTMNvGFX/last => FXXR_VT10vGFX | 0.532 | 0.531 | 0.579 | 0.511 | 0.538 | 0.525 | 0.069 | 0.0 | 0.050 | 0.000 | 0.0 | 0.531 |

As in the previous section, we will now evaluate the performance of the valuation-adjusted FX carry strategies. We will use the same naive PnL class as before, but this time we will use the relative FX carries as signals and the relative returns of the vol-targeted FX forward returns as targets.

#### Naive PnL#

```
dix = dict_crr_vgfx
sigx = dix["sigs"]
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", "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",
)
dix["pnls"] = naive_pnl
```

The `plot_pnls()`

method of the `NaivePnl()`

class is used to plot a line chart of cumulative PnL

```
dix = dict_crr_vgfx
start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx]
new_keys = [x + "vGFX_PZN" for x in dict_labs.keys()]
dict_labx = {new_key: dict_labs[old_key] for new_key, old_key in zip(new_keys, dict_labs)}
labx = [dict_labx[x] for x in pnls]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Naive PnLs of cross-currency valuation-adjusted FX carry strategies for all 25 currencies",
xcat_labels=labx,
figsize=(18, 10),
)
```

```
dix = dict_crr_vgfx
start = dix["start"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="2000-01-01",
)
```

The method `evaluate_pnls()`

returns a small dataframe of key PnL statistics. For definitions of Sharpe and Sortino ratios, please see here

```
display(df_eval.transpose().astype("float").round(3))
```

Return (pct ar) | St. Dev. (pct ar) | Sharpe Ratio | Sortino Ratio | Max 21-day draw | Max 6-month draw | USD_EQXR_NSA correl | EUR_FXXR_NSA correl | Traded Months | |
---|---|---|---|---|---|---|---|---|---|

xcat | |||||||||

FXCRR_NSA_5DMMW25vGFX_PZN | 4.821 | 10.0 | 0.482 | 0.764 | -11.521 | -18.511 | 0.097 | -0.001 | 288.0 |

FXCRR_NSA_PvBMvLTM_3AvGFX_PZN | 8.209 | 10.0 | 0.821 | 1.310 | -14.091 | -16.614 | 0.032 | -0.109 | 288.0 |

FXCRR_NSA_PvBMvLTM_6AvGFX_PZN | 7.111 | 10.0 | 0.711 | 1.148 | -13.167 | -14.492 | 0.066 | -0.061 | 288.0 |

FXCRR_NSA_PvBMvSLTM_3AvGFX_PZN | 7.972 | 10.0 | 0.797 | 1.293 | -11.866 | -17.220 | 0.063 | -0.061 | 288.0 |

FXCRR_NSA_PvBMvSLTM_6AvGFX_PZN | 7.366 | 10.0 | 0.737 | 1.201 | -12.148 | -16.554 | 0.081 | -0.036 | 288.0 |

### Directional adjusted hedged carry#

A final version of real carry strategy uses the real carry adjusted for hedge costs as signal to set up hedged FX forward positions, i.e., positions in portfolios with an FX forward versus benchmark currencies as the main leg and position in the hedge basket determined by the FX forward’s estimated beta up to the day as the secondary leg.

#### Specs and panel test#

```
cr_type = "FXCRRHvGDRB_NSA"
feats = [cr for cr in crotx if cr_type in cr] + [ov + "N" for ov in ovrts]
targ = "FXXRHvGDRB_NSA"
cidx = cids_fx
start = "2000-01-01"
dict_crh = {
"sigs": feats,
"targ": targ,
"cidx": cidx,
"start": start,
"black": fxblack,
"srr": None,
"pnls": None,
}
```

```
dix = dict_crh
sig = "FXCRRHvGDRB_NSA_5DMMW25"
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Real FX carry, adjusted for hedge basket carry, smoothed and winsorized, quarter-end",
ylab="Return on hedged FX forward position, next quarter",
title="Real hedged FX carry and subsequent hedged FX returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

```
dix = dict_crh
sig = dix["sigs"][0]
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab="Valuation-adjusted real FX carry adjusted for hedge basket carry, smoothed and winsorized, quarter-end",
ylab="Return on hedged FX forward position, next quarter",
title="Valuation-adj. real hedged FX carry and subseq. hedged FX returns, 25 currencies since 2000",
size=(8, 8),
prob_est="map"
)
```

#### Accuracy and correlation check#

```
dix = dict_crh
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=dix["sigs"],
sig_neg=[False, False, False, False, False, False, False],
rets=targ,
freqs="M",
start="2000-01-01",
blacklist=fxblack,
)
dix["srr"] = srr
dix = dict_crh
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 | map_pval | auc | |
---|---|---|---|---|---|---|---|---|---|---|---|---|

FXCRRHvGDRB_NSA_PvBMvLTM_3A | 0.540 | 0.546 | 0.336 | 0.502 | 0.563 | 0.529 | 0.109 | 0.0 | 0.077 | 0.0 | 0.0 | 0.541 |

FXCRRHvGDRB_NSA_PvBMvLTM_6A | 0.542 | 0.546 | 0.352 | 0.502 | 0.562 | 0.530 | 0.106 | 0.0 | 0.079 | 0.0 | 0.0 | 0.542 |

FXCRRHvGDRB_NSA_PvBMvSLTM_3A | 0.539 | 0.541 | 0.614 | 0.502 | 0.534 | 0.548 | 0.097 | 0.0 | 0.067 | 0.0 | 0.0 | 0.539 |

FXCRRHvGDRB_NSA_PvBMvSLTM_6A | 0.546 | 0.546 | 0.539 | 0.502 | 0.545 | 0.548 | 0.102 | 0.0 | 0.074 | 0.0 | 0.0 | 0.546 |

FXCRRHvGDRB_NSA_5DMMW25 | 0.541 | 0.543 | 0.401 | 0.502 | 0.554 | 0.533 | 0.091 | 0.0 | 0.069 | 0.0 | 0.0 | 0.542 |

PPPFXOVERVALUE_NSA_PvBMvLTMN | 0.517 | 0.519 | 0.376 | 0.503 | 0.526 | 0.511 | 0.063 | 0.0 | 0.035 | 0.0 | 0.0 | 0.517 |

PPPFXOVERVALUE_NSA_PvBMvSLTMN | 0.522 | 0.526 | 0.717 | 0.503 | 0.518 | 0.534 | 0.057 | 0.0 | 0.038 | 0.0 | 0.0 | 0.521 |

#### Naive PnL#

`NaivePnl()`

class is used again as for previous strategies:

```
dix = dict_crh
sigx = dix["sigs"]
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", "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
```

The `plot_pnls()`

method of the `NaivePnl()`

class is used to plot a line chart of cumulative PnL

```
dix = dict_crh
start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx]
new_keys = [x.replace("FXCRR", "FXCRRHvGDRB") + "_PZN" for x in dict_labs.keys()]
dict_labx = {new_key: dict_labs[old_key] for new_key, old_key in zip(new_keys, dict_labs)}
labx = [dict_labx[x] for x in pnls]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Naive PnLs of hedged valuation-adjusted FX carry strategies for all 25 currencies",
xcat_labels=labx,
figsize=(18, 10),
)
```

The method `evaluate_pnls()`

returns a small dataframe of key PnL statistics.

```
dix = dict_crh
start = dix["start"]
sigx = dix["sigs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="2000-01-01",
)
```

```
display(df_eval.transpose())
```

Return (pct ar) | St. Dev. (pct ar) | Sharpe Ratio | Sortino Ratio | Max 21-day draw | Max 6-month draw | USD_EQXR_NSA correl | EUR_FXXR_NSA correl | Traded Months | |
---|---|---|---|---|---|---|---|---|---|

xcat | |||||||||

FXCRRHvGDRB_NSA_5DMMW25_PZN | 7.208533 | 10.0 | 0.720853 | 1.091382 | -10.048142 | -20.430573 | 0.112217 | -0.148435 | 289 |

FXCRRHvGDRB_NSA_PvBMvLTM_3A_PZN | 8.33643 | 10.0 | 0.833643 | 1.261603 | -14.149351 | -30.957931 | 0.075038 | -0.238063 | 289 |

FXCRRHvGDRB_NSA_PvBMvLTM_6A_PZN | 8.708612 | 10.0 | 0.870861 | 1.319588 | -13.729806 | -29.821649 | 0.102422 | -0.216328 | 289 |

FXCRRHvGDRB_NSA_PvBMvSLTM_3A_PZN | 6.427117 | 10.0 | 0.642712 | 1.019678 | -13.733837 | -20.699637 | -0.022077 | 0.061433 | 289 |

FXCRRHvGDRB_NSA_PvBMvSLTM_6A_PZN | 7.788788 | 10.0 | 0.778879 | 1.233566 | -12.440269 | -22.314577 | 0.030496 | -0.020792 | 289 |

PPPFXOVERVALUE_NSA_PvBMvLTMN_PZN | 4.246341 | 10.0 | 0.424634 | 0.624596 | -13.474505 | -30.301893 | 0.009183 | -0.158534 | 289 |

PPPFXOVERVALUE_NSA_PvBMvSLTMN_PZN | 3.23389 | 10.0 | 0.323389 | 0.485595 | -14.194124 | -23.457628 | -0.096607 | 0.178986 | 289 |

### Relative adjusted hedged carry#

Finally, one can apply overvaluation adjustment to hedged cross-currency strategies, i.e., relative positions in the local currency forward versus the dominant benchmark and versus a 25-currency basket of these positions and hedged against global directional market risk. This means we apply hedged relative real carry with respect to hedged relative positions.

```
cr_type = "FXCRRHvGDRB_NSA"
feats = [cr + "vGFX" for cr in crotx if cr_type in cr] + [ov + "NvGFX" for ov in ovrts]
targ = "FXXRHvGDRB_NSA"
cidx = cids_fx
start = "2000-01-01"
dict_crh_vgfx = {
"sigs": feats,
"targ": targ,
"cidx": cidx,
"start": start,
"black": fxblack,
"srr": None,
"pnls": None,
}
```

```
dix = dict_crr_vgfx
sig = 'FXCRRHvGDRB_NSA_5DMMW25vGFX'
targ = dix["targ"]
cidx = dix["cidx"]
crx = msp.CategoryRelations(
dfx,
xcats=[sig, targ],
cids=cidx,
freq="Q",
lag=1,
xcat_aggs=["last", "sum"],
start="2000-01-01",
blacklist=fxblack,
xcat_trims=[None, None],
)
crx.reg_scatter(
labels=False,
coef_box="lower right",
xlab=None,
ylab=None,
title=None,
size=(8, 8),
prob_est="map"
)
```

#### Accuracy and correlation check#

As before, the `SignalReturnRelations`

class from the macrosynergy.signal module is used to analyze, visualize, and compare the relationships between panels of trading signals and panels of subsequent returns.

```
dix = dict_crh_vgfx
sigs = dix["sigs"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=sigs,
sig_neg=[False, False, False, False, False, False, False],
rets=targ,
freqs="M",
start="2000-01-01",
blacklist=fxblack,
)
dix["srr"] = srr
dix = dict_crh_vgfx
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 | map_pval | auc | |
---|---|---|---|---|---|---|---|---|---|---|---|---|

FXCRRHvGDRB_NSA_PvBMvLTM_3AvGFX | 0.534 | 0.534 | 0.501 | 0.502 | 0.536 | 0.532 | 0.096 | 0.000 | 0.064 | 0.000 | 0.000 | 0.534 |

FXCRRHvGDRB_NSA_PvBMvLTM_6AvGFX | 0.539 | 0.539 | 0.486 | 0.502 | 0.543 | 0.536 | 0.095 | 0.000 | 0.069 | 0.000 | 0.000 | 0.539 |

FXCRRHvGDRB_NSA_PvBMvSLTM_3AvGFX | 0.535 | 0.535 | 0.505 | 0.502 | 0.537 | 0.533 | 0.084 | 0.000 | 0.056 | 0.000 | 0.000 | 0.535 |

FXCRRHvGDRB_NSA_PvBMvSLTM_6AvGFX | 0.539 | 0.539 | 0.470 | 0.502 | 0.544 | 0.535 | 0.091 | 0.000 | 0.065 | 0.000 | 0.000 | 0.539 |

FXCRRHvGDRB_NSA_5DMMW25vGFX | 0.540 | 0.541 | 0.456 | 0.502 | 0.546 | 0.535 | 0.085 | 0.000 | 0.066 | 0.000 | 0.000 | 0.541 |

PPPFXOVERVALUE_NSA_PvBMvLTMNvGFX | 0.506 | 0.506 | 0.524 | 0.503 | 0.509 | 0.503 | 0.043 | 0.001 | 0.020 | 0.015 | 0.000 | 0.506 |

PPPFXOVERVALUE_NSA_PvBMvSLTMNvGFX | 0.524 | 0.524 | 0.579 | 0.503 | 0.523 | 0.525 | 0.041 | 0.001 | 0.028 | 0.001 | 0.001 | 0.523 |

#### Naive PnL#

`NaivePnl()`

class is used again as for previous strategies:

```
dix = dict_crh_vgfx
sigx = dix["sigs"]
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", "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",
)
dix["pnls"] = naive_pnl
```

The `plot_pnls()`

method of the `NaivePnl()`

class is used to plot a line chart of cumulative PnL

```
dix = dict_crh_vgfx
start = dix["start"]
cidx = dix["cidx"]
sigx = dix["sigs"][:-2]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigx]
new_keys = [x.replace("FXCRR", "FXCRRHvGDRB") + "vGFX_PZN" for x in dict_labs.keys()]
dict_labx = {new_key: dict_labs[old_key] for new_key, old_key in zip(new_keys, dict_labs)}
labx = [dict_labx[x] for x in pnls]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Naive PnLs of hedged cross-currency valuation-adjusted FX carry strategies for all 25 currencies",
xcat_labels=labx,
figsize=(18, 10),
)
```

```
dix = dict_crh_vgfx
start = dix["start"]
sigx = dix["sigs"]
naive_pnl = dix["pnls"]
pnls = [sig + "_PZN" for sig in sigx]
df_eval = naive_pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="2000-01-01",
)
```

The method `evaluate_pnls()`

returns a small dataframe of key PnL statistics. For definitions of Sharpe and Sortino ratios, please see here

```
display(df_eval.transpose())
```

Return (pct ar) | St. Dev. (pct ar) | Sharpe Ratio | Sortino Ratio | Max 21-day draw | Max 6-month draw | USD_EQXR_NSA correl | EUR_FXXR_NSA correl | Traded Months | |
---|---|---|---|---|---|---|---|---|---|

xcat | |||||||||

FXCRRHvGDRB_NSA_5DMMW25vGFX_PZN | 7.36261 | 10.0 | 0.736261 | 1.112948 | -10.913978 | -13.24192 | 0.066144 | -0.223532 | 289 |

FXCRRHvGDRB_NSA_PvBMvLTM_3AvGFX_PZN | 8.885118 | 10.0 | 0.888512 | 1.35584 | -14.29617 | -17.958328 | 0.033086 | -0.25849 | 289 |

FXCRRHvGDRB_NSA_PvBMvLTM_6AvGFX_PZN | 8.829762 | 10.0 | 0.882976 | 1.344693 | -12.503802 | -16.420116 | 0.052328 | -0.259085 | 289 |

FXCRRHvGDRB_NSA_PvBMvSLTM_3AvGFX_PZN | 7.566713 | 10.0 | 0.756671 | 1.168869 | -12.455905 | -13.427306 | -0.017983 | -0.215263 | 289 |

FXCRRHvGDRB_NSA_PvBMvSLTM_6AvGFX_PZN | 8.12719 | 10.0 | 0.812719 | 1.251513 | -11.394479 | -12.192728 | 0.017114 | -0.234369 | 289 |

PPPFXOVERVALUE_NSA_PvBMvLTMNvGFX_PZN | 4.039288 | 10.0 | 0.403929 | 0.59861 | -11.039638 | -19.99859 | -0.034249 | -0.056909 | 289 |

PPPFXOVERVALUE_NSA_PvBMvSLTMNvGFX_PZN | 4.982049 | 10.0 | 0.498205 | 0.752081 | -12.764516 | -15.981752 | -0.073799 | -0.089321 | 289 |