Fiscal policy criteria for fixed-income allocation #

This notebook serves as an illustration of the points discussed in the post “Fiscal policy criteria for fixed-income allocation” available on the Macrosynergy website.

The fiscal stance of governments can be a powerful force in local fixed-income markets. On its own, an expansionary stance is seen as a headwind for long-duration or government bond positions due to increased debt issuance, greater default or inflation risk, and less need for monetary policy stimulus. Quantamental indicators of general government balances and estimated fiscal stimulus allow backtesting the impact of fiscal stance information. Empirical evidence for 20 countries since the early 2000s shows that returns on interest rate swap receiver positions in fiscally more expansionary countries have significantly underperformed those in fiscally more conservative countries. Indicators of fiscal stance have been timely, theoretically plausible, and profitable criteria for fixed-income allocations across currency areas.

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 that are used throughout the analysis. It checks data availability, and blacklist periods (if any).

  • Transformations and Checks: In this part, the notebook performs various calculations and transformations on the data to derive the relevant signals and targets used for the analysis, including building composite fiscal score, relative fiscal indicators, and other metrics or ratios used in the analysis.

  • 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 various trading strategies targeting fixed income returns. The strategies utilize the indicators and other signals derived in the previous section.

It’s important to note that while the notebook covers a selection of indicators and strategies used for the post’s main findings, there are countless other possible indicators and approaches that can be explored by users, as mentioned in the post. Users can modify the code to test different hypotheses and strategies based on their own research and ideas. 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 used in the notebook.

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

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

# Run only if needed!
"""!pip install macrosynergy --upgrade"""
'!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 .

cids = [
    "AUD",
    "CAD",
    "CHF",
    "CNY",
    "EUR",
    "GBP",
    "HKD",
    "INR",
    "JPY",
    "KRW",
    "MXN",
    "MYR",
    "PLN",
    "SEK",
    "SGD",
    "THB",
    "TRY",
    "TWD",
    "USD",
    "ZAR",
]
# Categories

main = [
    "GGOBGDPRATIO_NSA",
    "GGSBGDPRATIO_NSA",
    "GGFTGDPRATIO_NSA",
    "EXALLOPENNESS_NSA_1YMA",
]

rets = [
    "DU05YXR_NSA",
    "DU05YXR_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 120

JPMaQS indicators are conveniently grouped into 6 main categories: Economic Trends, Macroeconomic balance sheets, Financial conditions, Shocks and risk measures, Stylized trading factors, and Generic returns. Each indicator has a separate page with notes, description, availability, statistical measures, and timelines for main currencies. The description of each JPMaQS category is available under Macro quantamental academy . For tickers used in this notebook see General government finance ratios , Economic openness ratios , and Duration returns .

start_date = "2000-01-01"
end_date = "2023-05-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,
        end_date=end_date,
        suppress_warning=True,
        metrics=["all"],
        show_progress=True,
    )
Downloading data from JPMaQS.
Timestamp UTC:  2024-03-27 11:15:39
Connection successful!
Requesting data: 100%|██████████| 24/24 [00:04<00:00,  4.95it/s]
Downloading data:   0%|          | 0/24 [00:00<?, ?it/s]
Downloading data: 100%|██████████| 24/24 [00:13<00:00,  1.78it/s]
Some expressions are missing from the downloaded data. Check logger output for complete list.
8 out of 480 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()`.

Availability #

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

msm.check_availability(df, xcats=main, cids=cids)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/e78f408194454f3045a73b60470c94063b54d4d01bcba93907dbc297513dee64.png https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/6573b0c3f46dcdeaffe017342289e563313cb6f248acd988471e737c4cdb0408.png

Take out bad-data return periods for fixed income markets #

dfx = df.copy()
filt_du = dfx["xcat"].isin(["DU05YXR_NSA", "DU05YXR_VT10"])
filt_try = (dfx["cid"] == "TRY") & (dfx["real_date"] > pd.to_datetime("2022-08-01"))
filt_myr = (
    (dfx["cid"] == "MYR")
    & (dfx["real_date"] > pd.to_datetime("2014-02-01"))
    & (dfx["real_date"] < pd.to_datetime("2014-07-01"))
)

filt_all = filt_du & (filt_try | filt_myr)

dfx.loc[filt_all, "value"] = np.nan

The graph presented below indicates the rationale behind excluding two periods for the Turkish Lira (TRY) starting from 2022 due to a significant spike in the data. Additionally, it highlights the lack of data available for the Malaysian Ringgit (MYR) in the year 2014.

msp.view_timelines(
    df,
    xcats=["DU05YXR_VT10"],
    cids=["TRY", "MYR"],
    ncol=2,
    cumsum=False,
    start="2014-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="Duration return for 10% vol target: 5-year maturity",
   
)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/97b36fce393272972d2d8155c314373dda5ca76b8ee15b0e2894baf82032189d.png

Transformations and checks #

Features #

For tracking the fiscal stance in a comparable fashion across countries we focus on two sets of quantamental indicators: The first set is the general government balance ratios defined as the sum of central, state and local governments as a percent of GDP. More details and timelines are under General government finance ratios . The second set of indicators is fiscal thrust, also called fiscal stimulus.

Balances and gaps #

cidx = cids
xcatx = ["GGOBGDPRATIO_NSA", "GGSBGDPRATIO_NSA"]

msp.view_timelines(
    df,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="General government balances, % of GDP, concurrent year, daily information states",
    xcat_labels=["Overall balance", "Structural balance"],
    title_fontsize=28
)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/dae9ae1a179c1b73700ed932444bfe80135ccf534935d9accf86c69574bb0c4b.png

We define Balance Gap as the average of overall and structural general government balance, % of GDP

calcs = [
    "BALS_GAP = GGSBGDPRATIO_NSA + 3 ",
    "BALO_GAP = GGOBGDPRATIO_NSA + 3 ",
    "BALA_GAP = ( BALS_GAP + BALO_GAP ) / 2",  # replace by linear combination
]

dfa = msp.panel_calculator(df, calcs, cids=cids)

dfx = msm.update_df(dfx, dfa)

Fiscal thrust #

General government fiscal thrust : Fiscal thrust, also called fiscal stimulus, measures the direct impact of fiscal policy on aggregate demand in the economy. Here it is approximated by the negative of the difference between a country’s (expected or estimated) structural balance as % of GDP in the current year versus the previous year. Since structural balances are adjusted for the impact of business cycles and terms-of-trade they are indicative of the stance of fiscal policy, i.e. the effect of discretionary fiscal measures on the economy. A positive value means that the structural balance shifts towards deficit and that fiscal policy looks expansionary. A negative value means that the structural balance has shifted towards surplus and that policy is restrictive.

calcs = [
    "THRUST = GGFTGDPRATIO_NSA ",
    "THRUST_AO = THRUST / EXALLOPENNESS_NSA_1YMA",
]

dfa = msp.panel_calculator(df, calcs, cids=cids)
dfx = msm.update_df(dfx, dfa)
cidx = cids
xcatx = ["THRUST"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=False,
    size=(12, 12),
    all_xticks=True,
    title="Fiscal thrust, % of GDP, concurrent year, daily information states",
  )
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/2babe3deca03b5fff7c361139a8967fe8d209fd54b5427d4db3f589b6c811896.png

Composite fiscal score #

To create a simple fiscal policy score that combines the two sets of indicators, namely the General Government Balance Ratios and General Government Fiscal Thrust, the following steps are taken:

Normalization: Each indicator (balance ratios and fiscal thrust) is normalized using z-scores. This process standardizes the data, making it easier to compare different countries on the same scale.

Winsorization: After normalization, extreme values are winsorized at 3 standard deviations. Winsorization involves capping or truncating extreme values to minimize the influence of outliers on the overall score.

Weighting: Each indicator is given a weight to determine its relative importance in the final score. In this case, both budget balances (General Government Balance Ratios) and fiscal thrust (General Government Fiscal Thrust) are given equal weights, each accounting for 50% of the overall score.

The resulting score provides a combined measure of a country’s fiscal policy performance, taking into account both its budget balances and the impact of fiscal measures on aggregate demand. By applying equal weights to both sets of indicators, the score ensures that each component contributes equally to the overall assessment of the country’s fiscal policy stance.

cidx = cids
xcatx = ["BALA_GAP", "THRUST"]

for xc in xcatx:
    dfa = msp.make_zn_scores(
        dfx,
        xcat=xc,
        cids=cidx,
        sequential=True,
        min_obs=2 * 261,
        neutral="zero",
        pan_weight=1,
        thresh=3,
        postfix="ZN",
    )
    dfx = msm.update_df(dfx, dfa)
dict_fcs = {
    "BALTHRUST_XCS": [
        ["BALA_GAPZN", "THRUSTZN"],
        None,
        [1, -1],
    ],
}

cidx = cids
dfa = pd.DataFrame(columns=dfx.columns)

for key, values in dict_fcs.items():
    dfaa = msp.linear_composite(
        dfx,
        xcats=values[0],
        weights=values[1],
        signs=values[2],
        cids=cidx,
        complete_xcats=False,
        new_xcat=key,
    )
    dfaa["xcat"] = key
    dfa = msm.update_df(dfa, dfaa)

dfx = msm.update_df(dfx, dfa)

Relative features & targets #

Balance and duration #

dict_new = {"fix": "vGLB_BAL"}
dict_new["xcats"] = [
    "BALA_GAP",
    "DU05YXR_VT10",
]
dfxx = dfx[dfx["xcat"].isin(dict_new["xcats"])]
dfxw = dfxx.pivot(index=["cid", "real_date"], columns="xcat", values="value")

available = dfxw.isna().any(axis=1).astype(int).groupby("cid").mean() == 1
dict_new["cids"] = list(available.index[available < 1].intersection(cids))

dfba = dfxw.isna().any(axis=1).astype(int).reset_index().rename({0: "value"}, axis=1)
dfba["xcat"] = "BLACK"
dict_new["black"] = msp.make_blacklist(dfba, "BLACK")

display(len(dict_new["cids"]), dict_new["cids"])

dict_bal = dict_new.copy()
20
['AUD',
 'CAD',
 'CHF',
 'CNY',
 'EUR',
 'GBP',
 'HKD',
 'INR',
 'JPY',
 'KRW',
 'MXN',
 'MYR',
 'PLN',
 'SEK',
 'SGD',
 'THB',
 'TRY',
 'TWD',
 'USD',
 'ZAR']

We use make_relative_value() function from the macrosynergy package to generate relative values for Balance Gap and Duration returns. “Relative” means that the original value is compared to a basket average. By default, the basket consists of all available cross-sections, and the relative value is calculated by subtracting the basket average from individual cross-section values. The new TimeSeries gets postfix vGLB_BAL as specified in the cell above

dix = dict_bal

dfa = msp.make_relative_value(
    dfx,
    xcats=dix["xcats"],
    cids=dix["cids"],
    start="2000-01-01",
    rel_meth="subtract",
    postfix=dix["fix"],
    blacklist=dix["black"],
)
dfx = msm.update_df(dfx, dfa)

Thrust and duration #

dict_new = {"fix": "vGLB_THR"}
dict_new["xcats"] = [
    "THRUST",
    "DU05YXR_VT10",
]
dfxx = dfx[dfx["xcat"].isin(dict_new["xcats"])]
dfxw = dfxx.pivot(index=["cid", "real_date"], columns="xcat", values="value")

available = dfxw.isna().any(axis=1).astype(int).groupby("cid").mean() == 1
dict_new["cids"] = list(available.index[available < 1].intersection(cids))

dfba = dfxw.isna().any(axis=1).astype(int).reset_index().rename({0: "value"}, axis=1)
dfba["xcat"] = "BLACK"
dict_new["black"] = msp.make_blacklist(dfba, "BLACK")
display(len(dict_new["cids"]), dict_new["cids"])

dict_thr = dict_new.copy()
20
['AUD',
 'CAD',
 'CHF',
 'CNY',
 'EUR',
 'GBP',
 'HKD',
 'INR',
 'JPY',
 'KRW',
 'MXN',
 'MYR',
 'PLN',
 'SEK',
 'SGD',
 'THB',
 'TRY',
 'TWD',
 'USD',
 'ZAR']

We use make_relative_value() function from the macrosynergy package to generate relative values for Thrust and Duration returns. As before, “relative” means that the original value is compared to a basket average. By default, the basket consists of all available cross-sections, and the relative value is calculated by subtracting the basket average from individual cross-section values. The new TimeSeries get postfix vGLB_THR as specified in the cell above

dix = dict_thr

dfa = msp.make_relative_value(
    dfx,
    xcats=dix["xcats"],
    cids=dix["cids"],
    start="2000-01-01",
    rel_meth="subtract",
    postfix=dix["fix"],
    blacklist=dix["black"],
)
dfx = msm.update_df(dfx, dfa)

Composite and duration #

Here we repeat the same process for the composite balance/thrust indicator and duration returns. We give the new TimeSeries postfix vGLB_CS

dict_new = {"fix": "vGLB_CS"}
dict_new["xcats"] = [
    "BALTHRUST_XCS",
    "DU05YXR_VT10",
]
dfxx = dfx[dfx["xcat"].isin(dict_new["xcats"])]
dfxw = dfxx.pivot(index=["cid", "real_date"], columns="xcat", values="value")

available = dfxw.isna().any(axis=1).astype(int).groupby("cid").mean() == 1
dict_new["cids"] = list(available.index[available < 1].intersection(cids))

dfba = dfxw.isna().any(axis=1).astype(int).reset_index().rename({0: "value"}, axis=1)
dfba["xcat"] = "BLACK"
dict_new["black"] = msp.make_blacklist(dfba, "BLACK")
display(len(dict_new["cids"]), dict_new["cids"])

dict_cs = dict_new.copy()
20
['AUD',
 'CAD',
 'CHF',
 'CNY',
 'EUR',
 'GBP',
 'HKD',
 'INR',
 'JPY',
 'KRW',
 'MXN',
 'MYR',
 'PLN',
 'SEK',
 'SGD',
 'THB',
 'TRY',
 'TWD',
 'USD',
 'ZAR']
dix = dict_cs

dfa = msp.make_relative_value(
    dfx,
    xcats=dix["xcats"],
    cids=dix["cids"],
    start="2000-01-01",
    rel_meth="subtract",
    postfix=dix["fix"],
    blacklist=dix["black"],
)
dfx = msm.update_df(dfx, dfa)

To display the results and to compare the absolute and relative balance/thrust indicator we use view_timelines function from the macrosynergy package

cidx = dict_cs["cids"]
xcatx = ["BALTHRUST_XCS", "BALTHRUST_XCSvGLB_CS"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=False,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,

)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/bc5d9f1b4f5701f0547d4ab707d529c7f29fec843a77c008cdf4889429293e54.png

Targets #

The target returns of the below analysis are fixed receiver positions in 5-year interest swaps, targeted at 10% volatility to allow comparable risk-taking across all currency areas, relative to a basket of the currency areas. Outperformance or underperformance of countries has often been persistent over months or even years.

cidx = cids
xcatx = ["DU05YXR_VT10vGLB_CS"]

msp.view_timelines(
    dfx,
    xcats=xcatx,
    cids=cidx,
    ncol=4,
    cumsum=True,
    start="2000-01-01",
    same_y=True,
    size=(12, 12),
    all_xticks=True,
    title="Cumulative returns of 5-year IRS receiver positions with 10% volatility target, relative to cross-country basket (monthly rebalancing)",
  )
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/b22463507f2be9dcb2e9f078fb2e0670a15b373dc0957912a17e961f9803ec39.png

Value checks #

In this part of the analysis, the notebook calculates the naive PnLs (Profit and Loss) for fixed income allocations, using the previously modified indicators. The PnLs are calculated based on simple trading strategies that utilize the created inflation measures as signals (no regression analysis is involved). The strategies involve going long (buying) or short (selling) on Equity positions based purely on the direction of the excess inflation signals.

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

  • Correlation: Measures the relationship between the 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 strategies in predicting market movements. Common 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 these fiscul indicator - based strategies with the long-only performance of the duration returns.

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.

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

Relative composite and duration #

# Change the sign of thrust indicator

dfx_thr = dfx[dfx["xcat"] == "THRUSTvGLB_THR"]
dfx_thr["value"] = -dfx_thr["value"]
dfx_thr["xcat"] = "THRUSTvGLB_THRN"
dfx = msm.update_df(dfx, dfx_thr)

As in previous notebooks, we create a dictionary for naive PnL creation: we define main trading signal (composite relative fiscal indicator), alternative signals (relative balance gap and thrust), target (relative duration return), relevant cross sections and few other parameters.

dix = dict_cs

dict_cs_rel = {
    "sig": "BALTHRUST_XCSvGLB_CS",
    "rivs": [
        "BALA_GAPvGLB_BAL",
        "THRUSTvGLB_THRN",
    ],
    "targ": "DU05YXR_VT10vGLB_CS",
    "cidx": dix["cids"],
    "black": dix["black"],
    "srr": None,
    "pnls": None,
}

with CategoryRelations() we can quickly visualise and analyse two categories (signal and target from the above dictionary). We define the lag and frequency (quarterly in our case)

dix = dict_cs_rel

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

crx = msp.CategoryRelations(
    dfx,
    xcats=[sig, targ],
    cids=cidx,
    freq="Q",
    lag=1,
    xcat_aggs=["last", "sum"],
    start="2000-01-01",
    blacklist=blax,
)

Relative fiscal scores have indeed been positively correlated with subsequent relative IRS returns for the available panel of data. The probability of significance from 2000-2022 has been above 98% at monthly or quarterly frequencies. The correlation coefficient has been 5% at a monthly and 6% at a quarterly frequency, which is modest but respectable for a single low-frequency indicator that has not been optimized and uses no whatsoever market information.

crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    separator=2011,
    xlab=None,
    ylab=None,
    title=None,
    size=(10, 6),
)
crx.reg_scatter(
    labels=False,
    coef_box="lower left",
    xlab="Relative fiscal score at quarter end, with positive sign indicating fiscal tightening",
    ylab="5-year IRS fixed receiver return at 10% vol versus basket",
    title="Relative fiscal scores and subsequent relative IRS returns since 2000 across 20 countries",
    size=(10, 6),
)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/d5b732a675e8853e4d5febf5045e1d6becdcf474c4a6bac9be28a3449e3de3d8.png https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/c7425a4295b1815f9f5a29da4fcb291165655eeaa8fa9f8c47657266d8d69909.png
dix = dict_cs_rel

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

srr = mss.SignalReturnRelations(
    dfx,
    cids=list(set(cidx) - set(["HKD", "CNY"])),
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2002-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_cs_rel

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2002-01-01",
    blacklist=blax,
)

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

dix["pnls"] = naive_pnl
dix = dict_cs

dict_cs_rel = {
    "sig": "BALTHRUST_XCSvGLB_CS",
    "rivs": [
        "BALA_GAPvGLB_BAL",
        "THRUSTvGLB_THRN",
    ],
    "targ": "DU05YXR_VT10vGLB_CS",
    "cidx": dix["cids"],
    "black": dix["black"],
    "srr": None,
    "pnls": None,
}
dix = dict_cs_rel

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

srr = mss.SignalReturnRelations(
    dfx,
    cids=list(set(cidx) - set(["HKD", "CNY"])),
    sigs=[sig] + rivs,
    rets=targ,
    freqs="M",
    start="2002-01-01",
    blacklist=blax,
)

dix["srr"] = srr
dix = dict_cs_rel
srrx = dix["srr"]
display(srrx.summary_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: BALTHRUST_XCSvGLB_CS/last => DU05YXR_VT10vGLB_CS 0.505 0.505 0.509 0.513 0.518 0.492 0.051 0.001 0.021 0.042 0.505
Mean years 0.505 0.507 0.509 0.514 0.518 0.495 0.058 0.381 0.023 0.409 0.507
Positive ratio 0.500 0.545 0.500 0.682 0.636 0.409 0.773 0.591 0.682 0.455 0.545
Mean cids 0.505 0.503 0.508 0.513 0.493 0.485 0.039 0.407 0.011 0.495 0.501
Positive ratio 0.611 0.556 0.500 0.611 0.667 0.278 0.778 0.444 0.778 0.389 0.500
dix = dict_cs_rel
srrx = dix["srr"]
display(srrx.signals_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
Return Signal Frequency Aggregation
DU05YXR_VT10vGLB_CS BALTHRUST_XCSvGLB_CS M last 0.505 0.505 0.509 0.513 0.518 0.492 0.051 0.001 0.021 0.042 0.505
BALA_GAPvGLB_BAL M last 0.504 0.505 0.476 0.513 0.518 0.491 0.039 0.013 0.016 0.126 0.505
THRUSTvGLB_THRN M last 0.497 0.496 0.506 0.513 0.509 0.483 0.033 0.035 0.007 0.471 0.496
dix = dict_cs_rel
srrx = dix["srr"]
display(srrx.cross_section_table().astype("float").round(3))
accuracy bal_accuracy pos_sigr pos_retr pos_prec neg_prec pearson pearson_pval kendall kendall_pval auc
M: BALTHRUST_XCSvGLB_CS/last => DU05YXR_VT10vGLB_CS 0.505 0.505 0.509 0.513 0.518 0.492 0.051 0.001 0.021 0.042 0.505
Mean 0.505 0.503 0.508 0.513 0.493 0.485 0.039 0.407 0.011 0.495 0.501
PosRatio 0.611 0.556 0.500 0.611 0.667 0.278 0.778 0.444 0.778 0.389 0.500
AUD 0.496 0.495 0.762 0.500 0.497 0.492 0.146 0.019 0.027 0.524 0.496
CAD 0.484 0.494 0.742 0.477 0.474 0.515 0.014 0.830 0.010 0.813 0.496
CHF 0.539 0.511 0.859 0.547 0.550 0.472 0.066 0.296 0.010 0.807 0.505
EUR 0.492 0.485 0.625 0.523 0.512 0.458 0.065 0.303 0.020 0.640 0.486
GBP 0.477 0.469 0.332 0.488 0.447 0.491 0.034 0.588 0.015 0.723 0.473
INR 0.515 0.515 0.000 0.485 0.000 0.515 -0.090 0.201 -0.107 0.023 0.500
JPY 0.504 0.532 0.258 0.543 0.591 0.474 0.039 0.539 0.014 0.747 0.525
KRW 0.515 0.503 0.838 0.520 0.520 0.485 0.027 0.703 0.021 0.658 0.501
MXN 0.481 0.480 0.413 0.500 0.477 0.484 0.037 0.596 -0.011 0.808 0.481
MYR 0.512 0.520 0.374 0.527 0.553 0.488 0.116 0.100 0.043 0.365 0.519
PLN 0.539 0.546 0.344 0.508 0.568 0.524 0.048 0.444 0.046 0.276 0.542
SEK 0.492 0.478 0.758 0.516 0.505 0.452 0.084 0.180 0.050 0.236 0.484
SGD 0.520 0.495 0.794 0.539 0.537 0.452 0.091 0.193 0.036 0.445 0.497
THB 0.464 0.452 0.609 0.546 0.508 0.395 -0.035 0.621 -0.037 0.430 0.453
TRY 0.513 0.511 0.404 0.487 0.500 0.522 0.142 0.049 0.044 0.367 0.510
TWD 0.510 0.506 0.608 0.520 0.524 0.488 0.028 0.694 0.045 0.334 0.506
USD 0.523 0.525 0.141 0.484 0.528 0.523 -0.093 0.139 -0.051 0.224 0.512
ZAR 0.520 0.538 0.289 0.527 0.581 0.495 -0.014 0.828 0.029 0.493 0.531
dix = dict_cs_rel

sigx = [dix["sig"]] + dix["rivs"]
targ = dix["targ"]
cidx = dix["cidx"]
blax = dix["black"]

naive_pnl = msn.NaivePnL(
    dfx,
    ret=targ,
    sigs=sigx,
    cids=cidx,
    start="2002-01-01",
    blacklist=blax,
)

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

dix["pnls"] = naive_pnl

The value generated by simply allocating fixed-income risk according to the fiscal expansion score has been considerable and quite consistent across time. The below shows an approximate PnL based on the standard normalized fiscal scores across all 20 countries (for the periods when signals were available and the market was tradable) and monthly rebalancing of positions in accordance with signal values. Positions are changed on the first trading day of the month and are assumed to have been updated by the beginning of the second trading day. The PnL chart has been normalized to 10% annualized volatility. This PnL is “naïve” insofar as we do not consider transaction costs and standard risk-management tools.

dix = dict_cs_rel

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

naive_pnl.plot_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
    title="PnL of interest rate swap relative value strategy across 20 countries, based only on fiscal score",
    xcat_labels=None,
    figsize=(14, 8),
)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/d2b84407331a6f3219e3db8d04bdbb5490daa0b2bf26a9f885b68c4ded88179b.png
dix = dict_cs_rel

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

df_eval = naive_pnl.evaluate_pnls(
    pnl_cats=pnls,
    pnl_cids=["ALL"],
    start="2000-01-01",
)
display(df_eval)
xcat BALA_GAPvGLB_BAL_PZN BALTHRUST_XCSvGLB_CS_PZN THRUSTvGLB_THRN_PZN
Return (pct ar) 3.898879 5.482395 4.109722
St. Dev. (pct ar) 10.0 10.0 10.0
Sharpe Ratio 0.389888 0.54824 0.410972
Sortino Ratio 0.563344 0.835279 0.618422
Max 21-day draw -14.784395 -17.119984 -9.689453
Max 6-month draw -18.947605 -17.125382 -15.388996
Traded Months 257 257 257
dix = dict_cs_rel
sig = dix["sig"]
naive_pnl = dix["pnls"]

naive_pnl.signal_heatmap(
    pnl_name=sig + "_PZN",
    freq="m",
    start="2000-01-01",
    figsize=(16, 7),
    title="Relative fiscal policy scores: positive values indicate tightening and recommend long rates receiver positions",
)
https://macrosynergy.com/notebooks.build/trading-factors/fiscal-policy-criteria-for-fixed-income-allocation/_images/63bbb74fe9f98a8c802e4a27e85130f567409d78259eab17f24700c042ddcefa.png