Intervention liquidity effects #

This notebook serves as an illustration of the points discussed in the post “Intervention liquidity” available on the Macrosynergy website.

Unsterilized central bank interventions in foreign exchange and securities markets increase base money liquidity independently from demand. Thus, they principally affect the money price of all assets. Since intervention policies are often persistent, reported trends are valid predictors of future effects. If markets are not fully macro information efficient, sustained relative intervention liquidity trends, distinguishing more supportive from less supportive central banks, are plausible predictors of the future relative performance of assets across different currency areas. Indeed, empirical evidence suggests that past trends of estimated intervention liquidity help predict future relative return performance of equity index futures, long-long equity-duration positions, and FX positions across countries.

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 the normalization of feature variables using z-score 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 a few simple but powerful trading strategies targeting selected financial returns.

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. For example, this notebook focuses on the main developed markets. Similar analysis can be extended for other currencies and regions. Best of luck with your research!

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

# Uncomment below for new downloads
""""
%%capture
! pip install macrosynergy --upgrade"""
'"\n%%capture\n! pip install macrosynergy --upgrade'
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os

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

from macrosynergy.download import JPMaQSDownload

import warnings

warnings.simplefilter("ignore")

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

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

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

# Cross sections

cids_dm = ["AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NOK", "NZD", "SEK", "USD"]
cids_em = ["CLP", "CNY", "COP", "CZK", "HKD", "HUF", "IDR", "ILS", "INR", "KRW", "MXN", "PLN", "RON", "RUB", "SGD", "THB", "TRY", "TWD", "ZAR"]
cids = cids_dm + cids_em

cids_wild = ["CNY", "HKD", "RUB"]
cids_il = list(set(cids_dm).union(set(cids_em)) - set(cids_wild))

cids_dmeq = ["AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "SEK", "USD"]
cids_emeq = ["INR", "KRW", "MXN", "PLN", "SGD", "THB", "TRY", "TWD", "ZAR"]
cids_eq = cids_dmeq + cids_emeq

# Categories

main = [
    "CRESFXGDP_NSA_D1M1ML6", # Currency reserve expansion as % of GDP
    "MBASEGDP_SA_D1M1ML6", # Monetary base expansion as % of GDP
    "INTLIQGDP_NSA_D1M1ML3", #Intervention-driven liquidity expansion as % of GDP, diff over 3 months 
    "INTLIQGDP_NSA_D1M1ML6", #Intervention-driven liquidity expansion as % of GDP, diff over 6 months 
]

rets = [
    "DU05YXR_NSA",
    "DU05YXR_VT10",
    "EQXR_NSA",
    "EQXR_VT10",
    "FXTARGETED_NSA",
    "FXUNTRADABLE_NSA",
    "FXXR_VT10",
]

xcats = main + 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 319

JPMaQS indicators are conveniently grouped into six 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 Intervention liquidity , Duration returns , Equity index future returns , FX forward returns , and FX tradeability and flexibility .

In this notebook, we introduce several lists of currencies for the sake of convenience in our subsequent analysis:

  • cids_dm represents the currencies associated with developed markets.

  • cids_em represents the currencies associated with emerging markets.

  • cids_il is a composite list that contains unique currency identifiers found in either - cids_dm or cids_em , excluding three specific currencies: CNY, RUB, and HKD.

  • cids_eq is a collection of currency identifiers encompassing both developed and emerging markets currencies. It is utilized to test hypotheses related to future equity returns. This list comprises currencies from both cids_dm and cids_em for which equity future returns data is available.

#  Download series from J.P. Morgan DataQuery by tickers

start_date = "1990-01-01"
end_date = "2023-07-01"


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

with JPMaQSDownload(client_id=client_id, client_secret=client_secret) as dq:
    df = dq.download(
        tickers=tickers,
        start_date=start_date,
        suppress_warning=True,
        metrics=["all"],
        show_progress=True,
    )
Downloading data from JPMaQS.
Timestamp UTC:  2025-06-25 12:04:20
Connection successful!
Requesting data:  34%|███▍      | 27/80 [00:05<00:10,  4.94it/s]
Requesting data: 100%|██████████| 80/80 [00:16<00:00,  4.81it/s]
Downloading data: 100%|██████████| 80/80 [00:40<00:00,  2.00it/s]
Some expressions are missing from the downloaded data. Check logger output for complete list.
150 out of 1595 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()`.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1975648 entries, 0 to 1975647
Data columns (total 8 columns):
 #   Column        Dtype         
---  ------        -----         
 0   real_date     datetime64[ns]
 1   cid           object        
 2   xcat          object        
 3   value         float64       
 4   grading       float64       
 5   eop_lag       float64       
 6   mop_lag       float64       
 7   last_updated  datetime64[ns]
dtypes: datetime64[ns](2), float64(4), object(2)
memory usage: 120.6+ MB

Availability #

Prior to commencing any analysis, it is crucial to evaluate the accessibility of data. This evaluation serves several purposes, including identifying potential data gaps or constraints within the dataset. Such gaps can significantly influence the trustworthiness and accuracy of the analysis. Moreover, it aids in verifying that ample observations are accessible for each chosen category and cross-section. Additionally, it assists in establishing suitable timeframes for conducting the analysis.

msm.missing_in_df(df, xcats=xcats, cids=cids)
No missing XCATs across DataFrame.
Missing cids for CRESFXGDP_NSA_D1M1ML6:  ['HKD']
Missing cids for DU05YXR_NSA:            ['RON']
Missing cids for DU05YXR_VT10:           ['RON']
Missing cids for EQXR_NSA:               ['CLP', 'COP', 'CZK', 'HUF', 'IDR', 'ILS', 'NOK', 'NZD', 'RON', 'RUB']
Missing cids for EQXR_VT10:              ['CLP', 'COP', 'CZK', 'HUF', 'IDR', 'ILS', 'NOK', 'NZD', 'RON', 'RUB']
Missing cids for FXTARGETED_NSA:         ['USD']
Missing cids for FXUNTRADABLE_NSA:       ['USD']
Missing cids for FXXR_VT10:              ['HKD', 'USD']
Missing cids for INTLIQGDP_NSA_D1M1ML3:  ['HKD']
Missing cids for INTLIQGDP_NSA_D1M1ML6:  ['HKD']
Missing cids for MBASEGDP_SA_D1M1ML6:    ['HKD']
msm.check_availability(df, xcats=main, cids=cids), 
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/ece342505d1677264ba112e750eca243a8162629d15f585a209ce98679e95092.png https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/a64911727e4707cf51a0f5832a8109c2e643c6289d4b2df86ce83fb4205e3dbf.png
(None,)

The following cell extracts four standard columns (cross-section identifier, category, date, and value) and then consolidates them into a fresh dataframe. We won’t be considering any other metrics (such as grading, eop_lag, and mop_lag) for our analysis.

scols = ["cid", "xcat", "real_date", "value"]  # required columns
dfx = df[scols].copy()
dfx.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1975648 entries, 0 to 1975647
Data columns (total 4 columns):
 #   Column     Dtype         
---  ------     -----         
 0   cid        object        
 1   xcat       object        
 2   real_date  datetime64[ns]
 3   value      float64       
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 60.3+ MB

Blacklist dictionary #

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

dfb = df[df["xcat"].isin(["FXTARGETED_NSA", "FXUNTRADABLE_NSA"])].loc[
    :, ["cid", "xcat", "real_date", "value"]
]
dfba = (
    dfb.groupby(["cid", "real_date"])
    .aggregate(value=pd.NamedAgg(column="value", aggfunc="max"))
    .reset_index()
)
dfba["xcat"] = "FXBLACK"
fxblack = msp.make_blacklist(dfba, "FXBLACK")
fxblack
{'CHF': (Timestamp('2011-10-03 00:00:00'), Timestamp('2015-01-30 00:00:00')),
 'CNY': (Timestamp('1999-01-01 00:00:00'), Timestamp('2025-06-24 00:00:00')),
 'CZK': (Timestamp('2014-01-01 00:00:00'), Timestamp('2017-07-31 00:00:00')),
 'HKD': (Timestamp('1999-01-01 00:00:00'), Timestamp('2025-06-24 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')),
 '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('2025-06-24 00:00:00')),
 'SGD': (Timestamp('1999-01-01 00:00:00'), Timestamp('2025-06-24 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-07-31 00:00:00'))}

We also exclude the period of non-tradability for TRY (Turkish Lira) swaps. Here is the timeline and rationale behind this removal:

msp.view_timelines(
    dfx,
    xcats=["DU05YXR_VT10"],
    cids=["TRY"],
    cumsum=False,
    start="2018-01-01",
    same_y=False,
    size=(6, 3),
    all_xticks=True,
    title="Duration return for 10% vol target: 5-year maturity, TRY",
   
)

# we take out bad-data return periods for fixed income markets 
filt_try = (dfx["cid"] == "TRY") & (dfx["real_date"] > pd.to_datetime("2022-08-01"))
dfx.loc[filt_try, "value"] = np.nan
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/2d3609ef314ead5531c1dd364f5e7c1b3e06bcd507a9727f7d599b2ce8eb9aca.png

Transformations and checks #

Features #

Intervention-driven liquidity expansion as % of GDP #

Intervention liquidity expansion is the change in the monetary base induced by central bank open market operations. Real-time information indicators of intervention liquidity expansion are readily available on the J.P. Morgan Macrosynergy Quantamental System (JPMaQS). The time series shows at the end of each day the latest available officially released data of central bank activity, regardless of the actual release frequency and observation period. Here we compare two indicators: Intervention-driven liquidity expansion as % of GDP, difference: over 3 months and over 6 months and their means/standard deviations as well as timelines are displayed with the help of view_ranges() and view_timelines() functions from the macrosynergy package.

cidx = cids_il
xcatx = ["INTLIQGDP_NSA_D1M1ML3", "INTLIQGDP_NSA_D1M1ML6"]

msp.view_ranges(
    dfx,
    cids=cidx,
    xcats=xcatx,
    kind="bar",
    sort_cids_by="mean",
    title="Means and standard deviation of intervention liquidity expansion (% of GDP, 2000-2023)",
    xcat_labels=["over 3 months", "over 6 months"],
    ylab="change in % of GDP",
    start="2000-01-01",
)
msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    
    same_y=False,
    all_xticks=True,
    title="Intervention liquidity expansion, latest reported change, % of GDP.",
    xcat_labels=["over the past 3 months", "over the past 6 months"],
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/84b482266e6fad566ed0e379ace909076d8d2367b8cf3ad3182de0405d96ec24.png https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/790c9a4f6a96a53247cfad3bf9b70a2ded993ad8917b91359824695b214b5463.png

Here, we are checking the correlation between the last six months’ reported intervention liquidity expansion and the next 3 months’ reported intervention liquidity expansion (this is regulated by lag=3 option and by aggregation of daily data on a monthly basis). Correlation between past and future 3- or 6-month trends has been highly significant, with a 20-25% coefficient. CategoryRelations() is a tool that allows for quick visualization and analysis of two categories , i.e. two time-series panels. It allows to define reguired lag (delay of arrival) of the first (feature) category in base periods, the type of modification of the feature variable (differencing or percentage changes), as well as the removal of outliers. The details can be found here .

The .reg_scatter() method is convenient for visualizing the relationship between two categories, including the strength of the linear association and any potential outliers. By default, it includes a regression line with a 95% confidence interval, which can help assess the significance of the relationship.

cidx = cids_il
xcatx = ["INTLIQGDP_NSA_D1M1ML6", "INTLIQGDP_NSA_D1M1ML3"]
cr = msp.CategoryRelations(
    dfx,
    xcats=xcatx,
    cids=cids_il,
    freq="M",
    lag=3,
    xcat_aggs=["last", "last"],
    start="2000-01-01",
    xcat_trims=[None, None],
)
cr.reg_scatter(
    labels=False,
    coef_box="lower right",
    title="Recent intervention liquidity trend and subsequent trend",
    xlab="Last six months' reported intervention liquidity expansion, as % of GDP",
    ylab="Next three months' reported intervention liquidity expansion, as % of GDP",
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/60c77fabd5af2efcae937ef148a5430f6e5b4121d53f676de6015e9f3fce288b.png

make_relative_value() function from the macrosynergy package generates a data frame of relative values for selected categories. In particular, we are looking at the set of Intervention-driven liquidity expansion trends (diff over 3 months and over 6 months), monetary base expansion, and currency reserve expansion. The new relative indicators receive postfix vGEQ (versus Global Equity), or vGFX (versus Global FX). These postfixes are given to distinguish trading strategies later.

xcatx = [
    "INTLIQGDP_NSA_D1M1ML6",
    "INTLIQGDP_NSA_D1M1ML3",
    "MBASEGDP_SA_D1M1ML6",
    "CRESFXGDP_NSA_D1M1ML6",
 ]

l_cidx = [cids_eq, cids]
l_blax = [None, fxblack]
l_pfxs = ["vGEQ", "vGFX"]

for i, cids_ac in enumerate(l_cidx):
    cids_acx = list(set(cids_ac) - set(["CHF"]))
    dfa = msp.make_relative_value(
        dfx, xcats=xcatx, cids=cids_acx, postfix=l_pfxs[i], blacklist=l_blax[i]
    )
    dfx = msm.update_df(dfx, dfa, xcat_replace=True)

Targets #

As potential targets for this notebook, we will consider relative country equity index future returns, relative FX forward returns, and relative country long-long returns.

Whereas equity index future returns and FX forward returns can be taken straight from the JPMaQS database, the relative country long-long returns should be first calculated: In the subsequent analysis, we compute a basic equity duration metric by combining the returns of two elements: the Equity Index Future Return for a 10% volatility target ‘EQXR_VT10’ and the 5-year Maturity Duration Return for a 10% volatility target DU05YXR_VT10 .

A straightforward ‘long-long’ strategy typically entails the allocation of positions with a 10% volatility target in two distinct asset classes. This method involves allocating positions with a specified volatility target across asset classes and assessing returns, particularly in the context of equity and duration. The “long-long carry” approach and the subsequent calculations help evaluate and compare performance within a portfolio while considering risk targets. The analysis has been conducted for the 16 currency areas.

non_lls = ["CLP", "COP", "CZK", "HUF", "IDR", "ILS", "NOK", "NZD"]
cidx = list(set(cids_il) - set(non_lls))
calcs = [
    "EQDU_XR_VT10S = EQXR_VT10 + DU05YXR_VT10",
]

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

Here, we calculate relative returns for the following categories:

- `EQXR_VT10`  -  Equity index future return for 10% vol target
- `EQDU_XR_VT10S` - previously calculated long-long returns
- `FXXR_VT10` - FX forward return for 10% vol target: dominant cross

The first two relative returns are calculated against the basket of currencies in the list cids_eq , excluding CHF . It is a list of 16 most liquid developed and emerging market currencies. FXXR_VT10 relative return is calculated relative to all available currencies (subject to blacklisted time periods)

xcatx = ["EQXR_VT10", "EQDU_XR_VT10S", 
         "FXXR_VT10"
    ]

l_cidx = [cids_eq, cids_eq, cids]
l_blax = [None, None, fxblack]
l_pfxs = ["vGEQ", "vGEQ", "vGFX"]

for i, cids_ac in enumerate(l_cidx):
    cids_acx = list(set(cids_ac) - set(["CHF"]))
    dfa = msp.make_relative_value(
        dfx, xcats=[xcatx[i]], cids=cids_acx, postfix=l_pfxs[i], blacklist=l_blax[i]
    )
    dfx = msm.update_df(dfx, dfa, xcat_replace=True)

view_timelines() function from the macrosynergy package displays the side by side comparison of the absolute and relative long-long returns.

cidx = list(set(cids_eq) - set(["CHF"]))
xcatx = ["EQDU_XR_VT10S", "EQDU_XR_VT10SvGEQ"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=False,
    all_xticks=True,
    
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/3b78a5b8ab01651f67bb4406e8c1ab829db1795b67828e10e8be3499cfb1d14c.png

Value checks #

In this part of the analysis, the notebook calculates the naive PnLs (Profit and Loss) for a few simple relative trading strategies based on:

  • predictive power of intervention liquidity on country equity index returns relative to the return of a broad international basket;

  • relative liquidity trend as the signal for the relative performance of calibrated risk-parity equity-duration “long-long” positions;

  • the impact of relatively strong intervention liquidity expansion on relative local currency returns.

The PnLs are calculated based on simple trading strategies that utilize the created indicators as signals (no regression analysis is involved). The strategies involve going long (buying) or short (selling) on a chosen financial asset based purely on the direction of the signals.

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

  • Correlation: Measures the relationship between the inflation-based strategy returns and the actual 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 inflation-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.

It is 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.

The analysis in the post and sample code in the notebook is a proof of concept only, using the simplest design.

Relative country equity returns #

We investigate the predictive power of intervention liquidity on country equity index returns relative to the return of a broad international basket of 16 currency areas with liquid markets. These returns are in respective local currencies. The hypothesis would be that – all other things equal – local equity prices in currency areas with faster liquidity expansion should outperform. Each country’s position has been scaled at the beginning of each month to comply with an expected 10% annualized volatility target to make vol-based position risk comparable across developed and emerging markets.

The countries for this analysis are collected in the list cids_eq . They are:

  • Australia, Canada, the euro area, Japan, Sweden, Switzerland, the UK, and the U.S in the developed world, and

  • India, Korea, Mexico, Poland, Singapore, Taiwan, Turkey, Thailand, and South Africa in the emerging world.

The target variable is country equity index returns relative to a basket EQXR_VT10vGEQ . Tested features will include relative to basket intervention liquidity expansion (% of GDP), Monetary base expansion as % of GDP, and Currency reserve expansion as % of GDP. The main tested feature will be INTLIQGDP_NSA_D1M1ML6vGEQ (relative intervention-driven liquidity expansion as % of GDP, difference: over 6 months)

targ = "EQXR_VT10vGEQ"
feat = "INTLIQGDP_NSA_D1M1ML6vGEQ"
rivs = [
    "INTLIQGDP_NSA_D1M1ML3vGEQ",
    "MBASEGDP_SA_D1M1ML6vGEQ",
    "CRESFXGDP_NSA_D1M1ML6vGEQ",
 ]

xcats_sel = [targ, feat] + rivs
cids_sel = cids_eq

for xs in xcats_sel:
    cids_avl = dfx[dfx["xcat"] == xs]["cid"].unique()
    cids_sel = set(cids_sel).intersection(set(cids_avl))
print(cids_sel)
{'PLN', 'ZAR', 'MXN', 'GBP', 'SGD', 'THB', 'SEK', 'EUR', 'AUD', 'KRW', 'CAD', 'TWD', 'USD', 'JPY', 'INR', 'TRY'}

Then we use CategoryRelations() from macrosynergy package, a tool that allows for quick visualization and analysis of two categories , i.e. two time-series panels. It allows to define reguired lag (delay of arrival) of the first (feature) category in base periods, type of modification of the feature variable (differencing or percentage changes), as well as the removal of outliers. The details can be found here .

Another useful tool is .reg_scatter() method. It is convenient for visualizing the relationship between two categories, including the strength of the linear association and any potential outliers. By default, it includes a regression line with a 95% confidence interval, which can help assess the significance of the relationship.

cr = msp.CategoryRelations(
    dfx,
    xcats=[feat, targ],
    cids=cids_sel,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    xcat_trims=[None, None],
)
cr.reg_scatter(
    labels=False,
    coef_box="lower right",
    title="Intervention liquidity expansion and subsequent relative equity index returns (16 countries 2002-2022)",
    xlab="Intervention liquidity, change over last 6 reported months, % of GDP, relative to international basket",
    ylab="Next month's equity index return, % (for 10% vol target), relative to international basket",
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/eb41f3bff638d1526b3f2e60ecf988f8f62c48b6129838e6734954089e1c32d7.png

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.

srr = mss.SignalReturnRelations(
    dfx,
    cids=list(cids_sel),
    sigs=[feat] + rivs,
    rets=targ,
    freqs="M",
    start="2002-01-01",
)

The .summary_table() of the SignalReturnRelations class gives a short high-level snapshot of the strength and stability of the main signal relation.

display(srr.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: INTLIQGDP_NSA_D1M1ML6vGEQ/last => EQXR_VT10vGEQ 0.513 0.514 0.365 0.500 0.518 0.510 0.022 0.152 0.013 0.217 0.513
Mean years 0.514 0.515 0.370 0.499 0.519 0.512 0.027 0.461 0.014 0.519 0.515
Positive ratio 0.542 0.583 0.125 0.458 0.667 0.542 0.542 0.333 0.542 0.292 0.583
Mean cids 0.513 0.509 0.362 0.498 0.506 0.512 0.003 0.503 0.010 0.496 0.509
Positive ratio 0.688 0.688 0.250 0.500 0.500 0.562 0.500 0.250 0.500 0.312 0.688

For a comparative overview of the signal-return relationship across the main and rival signals, one can use the signals_table() method of the SignalReturnRelations class.

srr.signals_table()
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
EQXR_VT10vGEQ INTLIQGDP_NSA_D1M1ML6vGEQ M last 0.512857 0.514003 0.365238 0.500476 0.518253 0.509752 0.022125 0.151675 0.012708 0.216937 0.512985
INTLIQGDP_NSA_D1M1ML3vGEQ M last 0.510238 0.511189 0.364524 0.500476 0.514696 0.507681 0.031728 0.039773 0.012545 0.222915 0.510367
MBASEGDP_SA_D1M1ML6vGEQ M last 0.492619 0.492497 0.418333 0.500476 0.491747 0.493246 0.006146 0.690500 -0.008434 0.412456 0.492697
CRESFXGDP_NSA_D1M1ML6vGEQ M last 0.506429 0.506748 0.407381 0.500476 0.508475 0.505022 0.028406 0.065660 0.021487 0.036809 0.506517

NaivePnl() class from macrosynergy package is designed to provide a quick and simple overview of a stylized PnL profile of a set of trading signals.

The related make_pnl() method calculates and stores generic PnLs based on a range of signals and their transformations into positions. The positioning options include the choice of trading frequency, z-scoring, simple equal-size long-short positions (-1/1) thresholds to prevent outsized positions, and rebalancing slippage. The generated PnLs are, however, naive insofar as they do not consider trading costs and plausible risk management restrictions. Also, if a volatility scale is set, this is done so ex-post, mainly for the benefit of plotting different signals’ PnLs in a single chart.

A complementary method is make_long_pnl() , which calculates a “long-only” PnL based on a uniform long position across all markets at all times. This often serves as a benchmark for gauging the benefits of active trading.

We transform raw signal using the option “zn_score_pan”, 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.

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=[feat, rivs[0]],
    cids=cids_sel,
    start="2000-01-01",
 #   bms=["USD_EQXR_NSA"],
)
naive_pnl.make_pnl(
    feat,
    sig_op="binary",
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    pnl_name=feat + "_DIG",
)
naive_pnl.make_pnl(
    feat,
    sig_op="zn_score_pan",
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    thresh=2,
    pnl_name=feat + "_PZN",
)

The PnLs are plotted using .plot_pnls() method of the NaivePnl() class . These plots mainly inform on seasonality and stability of value generation under the assumption of negligible transaction costs.

The chart below illustrates that value generation of signal based on z-scores of relative liquidity trends would have produced a modest Sharpe ratio of 0.3 and Sortino ratio of 0.4 before transaction costs. Yet, as for the relative equity PnL, correlation with global equity benchmarks has been near zero or slightly negative, suggesting that intervention liquidity trends have been a helpful signal for “long-long” allocations across currency areas.

pnls = [feat + x for x in ["_PZN", "_DIG"]]

dict_labels = {"INTLIQGDP_NSA_D1M1ML6vGEQ_PZN": "z-scores",
               "INTLIQGDP_NSA_D1M1ML6vGEQ_DIG": "simple long/short -1/1"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2002-01-01",
    title="Equity RV PnL based on z-scores of relative intervention liquidity expansion",
    xcat_labels=dict_labels,
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/181121207cc3c69a7410169648b9908a099bf40b0359e00ec853eaa5bafb9ad7.png

The method evaluate_pnls() returns a small dataframe of key PnL statistics.

df_eval = naive_pnl.evaluate_pnls(pnl_cats=pnls, pnl_cids=["ALL"], start="2002-01-01",)
display(df_eval.astype("float").round(2))
xcat INTLIQGDP_NSA_D1M1ML6vGEQ_DIG INTLIQGDP_NSA_D1M1ML6vGEQ_PZN
Return % 2.78 2.42
St. Dev. % 10.18 10.30
Sharpe Ratio 0.27 0.23
Sortino Ratio 0.39 0.34
Max 21-Day Draw % -11.04 -9.53
Max 6-Month Draw % -13.23 -13.60
Peak to Trough Draw % -21.04 -20.31
Top 5% Monthly PnL Share 1.16 1.47
Traded Months 282.00 282.00

Relative country long-long trades #

Here, we test a hypothesis that a strong relative liquidity trend should also benefit the relative performance of calibrated risk-parity equity-duration “long-long” positions. That is because local markets should broadly benefit from liquidity supply. “Long-long” positions aim to take equal volatility-based risk on equity and duration exposure, benefiting from the negative correlation between the two legs of the trade in times where real economic shocks and accommodative central bank policies dominate (view post here). Specifically, here “long-long” returns have been calculated based on 10% volatility-targeted positions in the main local equity index future and 5-year interest rate swap fixed receivers. The analysis has been conducted for the same 16 currency areas that were used for the equity analysis above.

targ = "EQDU_XR_VT10SvGEQ"
feat = "INTLIQGDP_NSA_D1M1ML6vGEQ"
rivs = [
    "INTLIQGDP_NSA_D1M1ML3vGEQ",
    "MBASEGDP_SA_D1M1ML6vGEQ",
    "CRESFXGDP_NSA_D1M1ML6vGEQ",
]

xcats_sel = [targ, feat] + rivs
cids_sel = cids_il
for xs in xcats_sel:
    cids_avl = dfx[dfx["xcat"] == xs]["cid"].unique()
    cids_sel = set(cids_sel).intersection(set(cids_avl))
print(cids_sel), len(cids_sel)
{'PLN', 'ZAR', 'MXN', 'GBP', 'SGD', 'THB', 'SEK', 'EUR', 'AUD', 'KRW', 'CAD', 'TWD', 'USD', 'JPY', 'INR', 'TRY'}
(None, 16)

The correlation of relative intervention liquidity with the subsequent relative performance of “long-long” trades has been positive and significant.

cr = msp.CategoryRelations(
    dfx,
    xcats=[feat, targ],
    cids=cids_sel,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2002-01-01",
    xcat_trims=[None, None],
)
cr.reg_scatter(
    labels=False,
    coef_box="lower right",
    title="Intervention liquidity expansion and subsequent monthly relative long-long returns (16 countries, 2002-2022)",
    xlab="Intervention liquidity, change over last 6 reported months, % of GDP, relative to international basket",
    ylab="Next month's equity-duration risk parity return, % mr, relative to international basket",
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/3c214d0f6ff6245d7843dba7dec4b8ac25abe5dfc62cd599d663591532ec6664.png

The same analysis on a quarterly basis suggests that the liquidity signals are persistent over many months.

cr = msp.CategoryRelations(
    dfx,
    xcats=[feat, targ],
    cids=cids_sel,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    xcat_trims=[None, None],
    xcat1_chg="diff",
)
cr.reg_scatter(
    labels=False,
    coef_box="lower right",
    title="Intervention liquidity expansion and subsequent quarterly relative long-long returns (16 countries, 2002-2022)",
    xlab="Intervention liquidity, change over last 6 reported months, % of GDP, relative to international basket",
    ylab="Next quarter's equity-duration risk parity return, % mr, relative to international basket",
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/469c4eb20d6497a877f02e2df8a1615c50d6325732453e27991698a0bab301fb.png

The same class NaivePnl() class from macrosynergy package along with the related make_pnl() , make_long_pnl() , and .plot_pnls() are used to create naive pnl for relative country long-long stragegy.

naive_pnl = msn.NaivePnL(
    dfx, ret=targ, sigs=[feat], cids=cids_sel, start="2000-01-01", bms=["USD_EQXR_NSA"],
)
naive_pnl.make_pnl(
    feat,
    sig_op="binary",
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    pnl_name=feat + "_DIG",
)
naive_pnl.make_pnl(
    feat,
    sig_op="zn_score_pan",
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    thresh=2,
    pnl_name=feat + "_PZN",
)



dict_labels = {"INTLIQGDP_NSA_D1M1ML6vGEQ_PZN": "z-scores",
               "INTLIQGDP_NSA_D1M1ML6vGEQ_DIG": "simple long/short -1/1"}



pnls = [feat + x for x in ["_PZN", "_DIG"]]
naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2002-01-01",
    title="Long-long RV PnL based on z-scores of relative intervention liquidity expansion",
    xcat_labels=dict_labels,
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/99da692ad2dddc6418437c8dd81985641ad1b9538d74b532011fafe388bed3c5.png

Relative FX returns #

In this part, we test the correlation between relative intervention liquidity trends and relative returns on local-currency longs. We would expect a negative relationship. We [a] use monthly generic FX forward returns of the local currency of smaller countries against natural benchmarks (U.S. dollar for most and euro or euro/dollar basket for some Europeans) and [b] use these returns for 10% vol-targeted positions versus an equally-weighted basket of such positions to arrive at generic relative returns. The list of selected currencies is printed as the output of the cell below.

targ = "FXXR_VT10vGFX"  
feat = "INTLIQGDP_NSA_D1M1ML6vGFX"
rivs = [
    "INTLIQGDP_NSA_D1M1ML3vGFX",
    "MBASEGDP_SA_D1M1ML6vGFX",
    "CRESFXGDP_NSA_D1M1ML6vGFX",
]

xcats_sel = [targ, feat] + rivs
cids_sel = list(set(cids_il) - set(["EUR", "USD"]))  # only small countries
for xs in xcats_sel:
    cids_avl = dfx[dfx["xcat"] == xs]["cid"].unique()
    cids_sel = set(cids_sel).intersection(set(cids_avl))
print(cids_sel), len(cids_sel)
{'THB', 'NZD', 'AUD', 'JPY', 'INR', 'SEK', 'CZK', 'KRW', 'MXN', 'COP', 'ZAR', 'CLP', 'GBP', 'CAD', 'PLN', 'RON', 'ILS', 'HUF', 'NOK', 'TWD', 'IDR', 'TRY'}
(None, 22)

We proceed, as before, using CategoryRelations() from macrosynergy package for quick visualization and analysis of two categories , i.e., two time-series panels, in particular, relative FX forward return FXXR_VT10vGFX , as target, and relative intervention liquidity expansion INTLIQGDP_NSA_D1M1ML6vGFX as the signal.

cr = msp.CategoryRelations(
    dfx,
    xcats=[feat, targ],
    cids=cids_sel,
    blacklist=fxblack,
    freq="M",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2002-01-01",
    xcat_trims=[None, None],
)
cr.reg_scatter(
    labels=False,
    coef_box="lower right",
    title="Intervention liquidity expansion and subsequent relative FX returns (22 countries 2002-2022)",
    xlab="Intervention liquidity, change over last 6 reported months, % of GDP, relative to international basket",
    ylab="Next month's FX forward return, 10% vol target, % mr, relative to international basket",
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/cacd227b25cdc5b05b1cf3b8d0b1c0d50c30d9696e548ed9cd7df94813360779.png

Using NaivePnl() class from macrosynergy package as before we provide a quick and simple overview of a stylized PnL profile of a set of trading signals.

The related make_pnl() method again calculates and stores generic PnLs based on a range of signals and their transformations into positions. The positioning options include the choice of trading frequency, z-scoring, simple equal-size long-short positions (-1/1) thresholds to prevent outsized positions, and rebalancing slippage.

A complementary method is make_long_pnl() , which calculates a “long-only” PnL based on a uniform long position across all markets at all times. This often serves as a benchmark for gauging the benefits of active trading.

We transform raw signal using the option “zn_score_pan”, 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.

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=[feat, "INTLIQGDP_NSA_D1M1ML3vGFX"],
    cids=cids_sel,
    start="2000-01-01",
    bms=["USD_EQXR_NSA"],
)
naive_pnl.make_pnl(
    feat,
    sig_op="binary",
    sig_neg=True,
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    pnl_name=feat + "_DIG",
)
naive_pnl.make_pnl(
    feat,
    sig_op="zn_score_pan",
    neutral="zero",
    sig_neg=True,
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    thresh=2,
    pnl_name=feat + "_PZN",
)
naive_pnl.make_pnl(
    "INTLIQGDP_NSA_D1M1ML3vGFX",
    sig_op="zn_score_pan",
    neutral="zero",
    sig_neg=True,
    rebal_freq="monthly",
    vol_scale=10,
    rebal_slip=1,
    thresh=2,
    pnl_name="INTLIQGDP_NSA_D1M1ML3vGFX_PZN",
)
pnls = [feat + x for x in ["_PZN", "_DIG"]]
dict_labels={"INTLIQGDP_NSA_D1M1ML6vGFX_PZN": "z-scores",
               "INTLIQGDP_NSA_D1M1ML6vGFX_DIG": "simple long/short -1/1"}

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2002-01-01",
    title="FX RV PnL based on z-scores of relative intervention liquidity expansion",
    xcat_labels=dict_labels,
)
https://macrosynergy.com/notebooks.build/trading-factors/intervention-liquidity-effects/_images/5f0271d789f9b45da5c9c286bf8656cf5414bde6f4b4475cda16072c08e3e267.png

The method evaluate_pnls() returns a small dataframe of key PnL statistics.

df_eval = naive_pnl.evaluate_pnls(pnl_cats=pnls, pnl_cids=["ALL"], start="2002-01-01",)
display(df_eval.astype("float").round(2))
xcat INTLIQGDP_NSA_D1M1ML6vGFX_DIG INTLIQGDP_NSA_D1M1ML6vGFX_PZN
Return % 1.88 3.56
St. Dev. % 10.10 10.25
Sharpe Ratio 0.19 0.35
Sortino Ratio 0.26 0.51
Max 21-Day Draw % -15.12 -16.37
Max 6-Month Draw % -23.70 -24.55
Peak to Trough Draw % -57.22 -42.53
Top 5% Monthly PnL Share 2.36 1.30
USD_EQXR_NSA correl 0.04 0.07
Traded Months 282.00 282.00