Commodity carry as a trading signal #
This notebook serves as an illustration of the points discussed in the post “Commodity carry as a trading signal” available on the Macrosynergy website.
Commodity futures carry contains information on implicit subsidies, such as convenience yields and hedging premia. It becomes a valid trading signal when incorporating some adjustments for inflation, seasonal effects, and volatility. There has been strong evidence for predictive power of carry with respect to subsequent futures returns for a broad panel of 23 commodities from 2000 to 2023. Furthermore, stylized naïve PnLs based on carry signals point to material economic value, either on its own or whenever managing long commodity exposure. The predictive power and value generation of carry signals seems to be even stronger for relative crosscommodity trades.
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.

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 normalization of feature variables using zscore or building a simple linear composite indicator for commodities futures returns.

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. The primary focus is on three key propositions:

The viability of real commodity carry as a predictor for subsequent outright commodity futures returns.

The effectiveness of relative commodity carry in predicting subsequent global relative commodity futures returns.

The reliability of relative commodity carry as a predictor for subsequent intragroup relative commodity futures returns.

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. 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
.
# Uncomment below for new downloads or package updates
"""
%%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 warnings
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
warnings.simplefilter("ignore")
# Commodity crosssection lists
cids_nfm = ["GLD", "SIV", "PAL", "PLT"]
cids_fme = ["ALM", "CPR", "LED", "NIC", "TIN", "ZNC"]
cids_ene = ["BRT", "WTI", "NGS", "GSO", "HOL"]
cids_sta = ["COR", "WHT", "SOY", "CTN"]
cids_mis = ["CFE", "SGR", "NJO", "CLB"]
cids = cids_nfm + cids_fme + cids_ene + cids_sta + cids_mis
comm_groups = {
"PRM": ["GLD", "SIV", "PAL", "PLT"], # Precious metals
"INM": ["ALM", "CPR", "LED", "NIC", "TIN", "ZNC"], # Industrial metals
"ENE": ["BRT", "WTI", "NGS", "GSO", "HOL"], # Energy
"GRA": ["COR", "WHT", "SOY", "CTN"], # Grains
"SOF": ["CFE", "SGR", "NJO", "CLB"]
}
# Category tickers
main = [
# nominal carry
"COCRY_NSA", "COCRY_VT10", "COCRY_SA", "COCRY_SAVT10",
# real carry
"COCRR_NSA", "COCRR_VT10", "COCRR_SA", "COCRR_SAVT10"
]
econ = []
mark = ["COXR_NSA", "COXR_VT10"]
xcats = main + econ + mark
# Resultant tickers
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 232
The description of each JPMaQS category is available under Macro quantamental academy , or JPMorgan Markets (password protected). For tickers used in this notebook see Commodity future carry , Commodity future returns , Equity index future returns (USD) , and The global directional risk basket (GLB) .
# Download series from J.P. Morgan DataQuery by tickers
start_date = "19950101"
# 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:
dfd = downloader.download(
tickers=tickers,
start_date=start_date,
metrics=["value",],
suppress_warning=True,
show_progress=True,
)
Downloading data from JPMaQS.
Timestamp UTC: 20240321 12:58:08
Connection successful!
Requesting data: 100%██████████ 12/12 [00:02<00:00, 4.93it/s]
Downloading data: 100%██████████ 12/12 [00:13<00:00, 1.08s/it]
Some dates are missing from the downloaded data.
3 out of 7626 dates are missing.
dfx = dfd.copy().sort_values(["cid", "xcat", "real_date"])
dfx.info()
<class 'pandas.core.frame.DataFrame'>
Index: 1574676 entries, 158531 to 1574675
Data columns (total 4 columns):
# Column NonNull Count Dtype
   
0 real_date 1574676 nonnull datetime64[ns]
1 cid 1574676 nonnull object
2 xcat 1574676 nonnull object
3 value 1574676 nonnull float64
dtypes: datetime64[ns](1), float64(1), object(2)
memory usage: 60.1+ MB
Availability #
It is important to assess data availability before conducting any analysis. It allows to identify any potential gaps or limitations in the dataset, which can impact the validity and reliability of analysis and ensure that a sufficient number of observations for each selected category and crosssection is available as well as determining the appropriate time periods for analysis.
msm.check_availability(dfx, xcats=xcats, cids=cids, missing_recent=False)
Transformation and checks #
Features #
Nominal carry #
Futures carry displays extreme differences in variation across commodities. Natural gas, gasoline, and soy display large volatility, whereas easily storable precious metals, particularly palladium, gold, and silver, show very low variation. Here, we are displaying
COCRY_NSA
, Nominal commodity future carry. This indicator comes straight from JPMaQS and does not require any transformations. Convenient function
.view_ranges()
displays a barplot with historical means and standard deviations of the indicator from the chosen date (2000)
xcatx = ["COCRY_NSA"]
sdate = "20000101"
msp.view_ranges(
dfx,
cids=cids,
xcats=xcatx,
kind="bar",
sort_cids_by="std",
title=f"Means and standard deviations of commodity futures carry since {sdate}",
start=sdate,
legend_bbox_to_anchor=(1, 1)
)
Below are the actual means of futures carry across the panel of 25 commodities and the mean across all values:
filt =( dfx["xcat"] == "COCRY_NSA") & (dfx['real_date'] > "20000101")
dfw = (
dfx[filt]
.pivot_table(index="real_date", columns="cid", values="value")
.replace(0, np.nan)
)
display(dfw.mean().sort_values(ascending=False))
display(dfw.mean().mean())
cid
GSO 11.076610
SOY 7.517636
SGR 3.568312
TIN 1.603071
NIC 0.833597
HOL 0.665130
PLT 0.177149
CPR 0.318347
WTI 0.393550
PAL 0.935151
LED 0.987825
BRT 1.372177
GLD 2.320359
SIV 2.482233
ZNC 2.587650
NJO 2.648748
CTN 4.113172
CLB 4.122368
ALM 4.154135
NGS 4.954269
COR 6.373901
CFE 10.225343
WHT 11.093605
dtype: float64
1.4626664769309723
Real carry #
The line plot below shows the development of nominal and real carry across all 25 commodities with the help of customized function
view_timelines()
from the
macrosynergy
package. The differences between real and nominal measures are mostly minor but notable for commodities with low carry variance. Conveniently, the Real commodity future carry indicator
"COCRR_NSA"
is also directly obtainable from JPMaQS alongside nominal version
“COCRY_NSA”
xcatx = ["COCRY_NSA", "COCRR_NSA"]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Nominal and real commodity futures carry",
title_fontsize=30,
xcat_labels=["Nominal", "Real"],
legend_fontsize=18,
)
Inflation adjustment pushes the average panel carry since 2000 from negative to positive territory:
filt =( dfx["xcat"] == "COCRR_NSA") & (dfx['real_date'] > "20000101")
dfw = (
dfx[filt]
.pivot_table(index="real_date", columns="cid", values="value")
.replace(0, np.nan)
)
dfw.mean().mean()
0.8176231139133816
Commodity future carry is not highly correlated across markets, and hence, carrybased positions should tend to be more diversified. Below is a correlation matrix showing a rather weak correlation across most commodities with very few exceptions (such as between WTI and BRT). The
correl_matrix()
function from
macrosynergy
package visualizes Pearson correlation between commodity futures carry across different commodities:
msp.correl_matrix(
dfx,
xcats="COCRR_NSA",
cids=cids,
title="Crosscommodity correlation of real futures carry, since 2000",
size=(12, 8),
start=sdate,
cluster=False
)
Volatilityadjusted carry #
The average daily return standard deviations of natural gas futures are roughly three times as large as those of gold. To mitigate this heteroscedasticity, one can calculate “volatilitytargeted carry.” JPMaQS calculates such carry based on positions that are scaled to a 10% vol target based on a historical standard deviation of the commodity future returns for an exponential moving average with a halflife of 11 days. Positions are rebalanced at the end of each month. Conveniently the volatility adjusted carry is also available from JPMaQS
COCRR_VT10
. As before,
.view_ranges()
displays a barplot with historical means and standard deviations of the indicator from the chosen date (2000)
xcatx = ["COCRR_VT10"]
msp.view_ranges(
dfx,
cids=cids,
xcats=xcatx,
kind="bar",
sort_cids_by="std",
title=f"Median, normal ranges and outliers of voltargeted futures carry since {sdate}",
start=sdate,
legend_bbox_to_anchor=(1, 1)
)
Volatility targeted real futures carry are displayed for comparison with the help of customized function
view_timelines()
from the
macrosynergy
package:
xcatx = ["COCRR_VT10",]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Voltargeted real commodity futures carry",
title_fontsize=30,
legend_fontsize=18,
)
Seasonally adjusted carry #
Here, we display sidebyside nonseasonally adjusted real carry
COCRR_NSA
with seasonally adjusted version
COCRR_SA
, which also comes straight from JPMaQS and does not require any transformation. As before, the customized function
view_timelines()
from the
macrosynergy
package proves handy for side by side comparison of both indicators:
xcatx = ["COCRR_NSA", "COCRR_SA"]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=False,
start=sdate,
same_y=False,
all_xticks=True,
title="Real commodity futures carry (outright and seasonally adjusted)",
title_fontsize=30,
xcat_labels=["Outright", "Seasonally adjusted"],
legend_fontsize=18,
)
To display realtime estimated seasonal factors, we display the difference between seasonally adjusted and nonadjusted carry.
calcs = ["COCRY_ASF = COCRY_SA  COCRY_NSA",]
dfa = msp.panel_calculator(dfx, calcs=calcs, cids=cids)
dfx = msm.update_df(dfx, dfa)
msp.view_timelines(
dfx,
xcats="COCRY_ASF",
cids=cids,
ncol=4,
cumsum=False,
start=sdate,
same_y=True,
all_xticks=True,
title="Additive seasonal factors of commodity futures carry (in % ar)",
title_fontsize=30,
legend_fontsize=18,
)
Normalized carry #
Normalizing values across different categories is a common practice in macroeconomics. This is particularly important when summing or averaging categories with different units and time series properties. Using
macrosynergy's
custom function
make_zn_scores()
we normalize the selected scores around neutral value (zero), using only past information. Reestimation is done on monthly basis, and we use a minimum of 3 years data. We protect against outliers using 3 standard deviations as the threshold. The normalized indicators receive postfix
_ZNI
. Using
pan_weight=0
, we use a particular crosssection, not the whole panel, for scaling.
crrs = ["COCRR_NSA", "COCRR_VT10", "COCRR_SA", "COCRR_SAVT10"]
xcatx = crrs
dfa = pd.DataFrame(columns=list(dfx.columns))
for xc in xcatx:
dfaa = msp.make_zn_scores(
dfx,
xcat=xc,
cids=cids,
sequential=True,
min_obs=261 * 3, # minimum requirement is 3 years of daily data
neutral="zero",
pan_weight=0,
thresh=3,
postfix="_ZNI",
est_freq="m",
)
dfa = msm.update_df(dfa, dfaa)
dfx = msm.update_df(dfx, dfa)
xcatx = ["COCRR_SA_ZNI", "COCRR_SAVT10_ZNI"]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=False,
start=sdate,
same_y=True,
all_xticks=True,
title="Normalized real seasonallyadjusted futures carry (outright and voltargeted)",
title_fontsize=30,
xcat_labels=["Outright", "Voltargeted"],
legend_fontsize=18,
)
Relative carry metrics #
For the second hypothesis tested in this notebook (The effectiveness of relative commodity carry in predicting subsequent global relative commodity futures returns), we build global relative values of voltargeted carry. The
make_relative_value()
function generates a data frame of relative values for a given list of categories. In this case, “relative” means that the original value is compared to a basket average. By default, the basket consists of all available crosssections, and the relative value is calculated by subtracting the basket average from individual crosssection values. Here, we calculate relative voltargeted adjusted and nonadjusted futures commodity real carry. The new time series receives postfix
vGCO
(versus Global Commodities).
xcatx= ['COCRR_VT10_ZNI', 'COCRR_SAVT10_ZNI', 'COCRR_VT10', 'COCRR_SAVT10']
dfa = msp.make_relative_value(
dfx,
xcats=xcatx,
cids=cids,
blacklist=None,
rel_meth="subtract",
rel_xcats=None,
postfix="vGCO",
)
dfx = msm.update_df(dfx, dfa)
For the third hypothesis tested in this notebook (the reliability of relative commodity carry as a predictor for subsequent intragroup relative commodity futures returns), we build relative values of voltargeted carry against their relative groups. We distinguish:

precious metals (gold, silver, palladium, and platinum),

base metals (aluminum, copper, lead, nickel, tin, and zinc),

fuels (Brent, WTI, natural gas, gasoline and heating oil),

U.S. corn belt crops (cotton, corn, soy and wheat), and

other agricultural commodities (coffee, sugar, orange juice, and lumber)
The new category receives postfix
vRCO
(vs Relative Commodities)
xcatx= ['COCRR_VT10_ZNI', 'COCRR_SAVT10_ZNI', 'COCRR_VT10', 'COCRR_SAVT10']
dfa = pd.DataFrame(columns=list(dfx.columns))
for new_cid, cid_group in comm_groups.items():
dfaa = msp.make_relative_value(
dfx,
xcats=xcatx,
cids=cid_group,
blacklist=None,
rel_meth="subtract",
rel_xcats=None,
postfix="vRCO",
)
dfa = msm.update_df(dfa, dfaa)
dfx = msm.update_df(dfx, dfa)
Targets #
Directional returns #
Commodity future returns
"COXR_NSA"
is available directly from JPMaQS. The means across commodities and standard deviations since 2000 are displayed below with
.view_ranges()
function from the
macrosynergy
package
xcatx = ["COXR_NSA"]
msp.view_ranges(
dfx,
cids=cids,
xcats=xcatx,
kind="bar",
sort_cids_by="std",
title=f"Means and standard deviations of commodity futures returns since {sdate}",
start=sdate,
legend_bbox_to_anchor=(1, 1)
)
The correlation matrix of commodity futures returns displays a positive correlation since 2000:
msp.correl_matrix(
dfx,
xcats="COXR_NSA",
cids=cids,
title=None,
size=(12, 8),
start=sdate,
cluster=True
)
Cumulative volatilitytargeted commodity futures returns for all commodities can be quickly displayed with the help of the customized function
view_timelines()
from the
macrosynergy
package:
xcatx = ["COXR_VT10"]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=True,
start=sdate,
same_y=True,
all_xticks=True,
title="Voltargeted commodity futures return, % cumulative",
title_fontsize=30,
legend_fontsize=18,
)
Relative returns to entire commodity basket #
The
make_relative_value()
function generates a data frame of relative values for a given list of categories. In this case, “relative” means that the original value is compared to a basket average. By default, the basket consists of all available crosssections, and the relative value is calculated by subtracting the basket average from individual crosssection values. Here, we calculate relative voltargeted commodity future returns. The new time series receives postfix
vGCO
(versus Global commodities)  the same postfix as for relative futures commodity real carry calculated above.
xcatx = ["COXR_VT10"]
dfa = msp.make_relative_value(
dfx,
xcats=xcatx,
cids=cids,
blacklist=None,
rel_meth="subtract",
rel_xcats=None,
postfix="vGCO",
)
dfx = msm.update_df(dfx, dfa)
Relative returns to specific commodity basket #
Another way to look at relative return is to look at it relative not to all commodities but to respective commodity groups. As distinct groups, we choose the following:

"PRM"
 precious metals (gold, silver, palladium, and platinum), 
"INM"
 base metals (aluminum, copper, lead, nickel, tin, and zinc), 
"ENE"
 fuels (Brent, WTI, natural gas, gasoline and heating oil), 
"GRA"
 U.S. corn belt crops (cotton, corn, soy and wheat), and 
"SOF"
 other agricultural commodities (coffee, sugar, orange juice, and lumber).
Here, we calculate the relative return to the respective group, so if a commodity belongs to precious metals, we calculate the relative return to the average of the precious metals group, not to the average return of all commodities. The new category receives postfix
vRCO
(the same postfix as for relative intragroup commodities futures carry calculated above in this notebook)
xcatx = ["COXR_VT10"]
comm_groups = {
"PRM": ["GLD", "SIV", "PAL", "PLT"], # Precious metals
"INM": ["ALM", "CPR", "LED", "NIC", "TIN", "ZNC"], # Industrial metals
"ENE": ["BRT", "WTI", "NGS", "GSO", "HOL"], # Energy
"GRA": ["COR", "WHT", "SOY", "CTN"], # Grains
"SOF": ["CFE", "SGR", "NJO", "CLB"]
}
dfa = pd.DataFrame(columns=list(dfx.columns))
for new_cid, cid_group in comm_groups.items():
dfaa = msp.make_relative_value(
dfx,
xcats=xcatx,
cids=cid_group,
blacklist=None,
rel_meth="subtract",
rel_xcats=None,
postfix="vRCO",
)
dfa = msm.update_df(dfa, dfaa)
dfx = msm.update_df(dfx, dfa)
All three types of volatility targeted cumulative commodity returns:

“outright”,

“relative to global commodity average”, and

“relative to group commodity average”
are displayed for comparison with the help of customized function
view_timelines()
from the
macrosynergy
package:
xcatx = ["COXR_VT10", "COXR_VT10vGCO", "COXR_VT10vRCO"]
msp.view_timelines(
dfx,
xcats=xcatx,
cids=cids,
ncol=4,
cumsum=True,
start=sdate,
same_y=True,
all_xticks=True,
title="Voltargeted commodity futures return, % cumulative",
title_fontsize=30,
xcat_labels=["outright", "relative to global commodity average", "relative to group commodity average"],
legend_fontsize=18,
)
Value checks #
In this part of the analysis, the notebook calculates the naive PnLs (Profit and Loss) for commodity future returns using commodity carry as signals. The PnLs are calculated based on simple trading strategies that utilize the carry as signals (no regression is involved). The strategies involve going long (buying) or short (selling) on commodity positions based purely on the direction of the carry.
To evaluate the performance of these strategies, the notebook computes various metrics and ratios, including:

Correlation: Measures the relationship between the changes in carry and consequent commodity 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 carry based 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 three simple carrybased strategies with the longonly performance of commodity futures.
The three strategies investigated in this notebook are:

Directional strategy: real commodity carry as a predictor for subsequent outright commodity futures returns.

Relative to global commodity basket: relative commodity carry as a predictor of subsequent global relative commodity futures returns.

Relative to specific commodity group baskets: relative commodity carry as a predictor for subsequent intragroup relative commodity futures 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.
Directional #
This part of the notebook aims to investigate the initial hypothesis regarding the predictive capability of commodity carry in anticipating future outright commodity returns.
Specs and panel test #
We establish a dictionary encompassing a list of potential signals for initial data analysis and ease of use. The primary signal under consideration is
COCRR_SAVT10_ZNI
, which represents normalized real seasonallyadjusted futures carry with volatility targeting. Other signals to be explored include nonseasonally adjusted carry and nonvolatility targeted carry versions, as well as nonnormalized signals. The targeted return here is
COXR_VT10
, representing commodity future returns with volatility targeting.
sigs = [
"COCRR_SAVT10_ZNI",
"COCRR_SAVT10",
"COCRR_SA",
"COCRR_SA_ZNI"
"COCRR_NSA",
"COCRR_VT10",
"COCRR_VT10_ZNI",
"COCRR_NSA_ZNI",
]
targ = "COXR_VT10"
start = "20000101"
dict_dir = {
"sigs": sigs,
"targ": targ,
"cidx": cids,
"start": start,
"black": None,
"srr": None,
"pnls": None,
}
Using function
CategoryRelations()
from
macrosynergy
package we visualize the relationship between the main signal and the target. The function allows aggregation (last value for signal and sum for target), monthly reestimation frequency, and lag of 1 month (i.e., we estimate the relationship between the signal and subsequent target and thus test the signal’s predictive power). As the signal, we use seasonally adjusted voltargeted carry, normalized and windorized
COCRR_SAVT10_ZNI
and the target is commodity basket return
COXR_VT10
cr = msp.CategoryRelations(
dfx,
xcats=[sigs[0], targ],
cids=cids,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
fwin=1,
start=sdate,
)
cr.reg_scatter(
labels=False,
coef_box="lower right",
title = "Seasonally adjusted voltargeted commodity carry and subsequent futures returns",
xlab="Seasonallyadjusted voltargeted carry, normalized and winsorized, %ar",
ylab="Next month's voltargeted futures return, %ar",
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.
sigs = [
"COCRR_SAVT10_ZNI",
"COCRR_SAVT10",
"COCRR_SA",
"COCRR_SA_ZNI",
"COCRR_NSA",
"COCRR_VT10",
"COCRR_VT10_ZNI",
"COCRR_NSA_ZNI"
]
srr = mss.SignalReturnRelations(
dfx,
cids=cids,
sigs=sigs,
rets=targ,
freqs="M",
start="20000101",
)
The
.summary_table()
of the SignalReturnRelations class gives a short highlevel snapshot of the strength and stability of the main signal relation (seasonally adjusted voltargeted carry, normalized and windorized
COCRR_SAVT10_ZNI
) and the target is commodity basket return
COXR_VT10
.
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  

Panel  0.526  0.528  0.469  0.522  0.551  0.505  0.045  0.000  0.041  0.000  0.528 
Mean years  0.529  0.535  0.466  0.521  0.564  0.506  0.049  0.332  0.044  0.329  0.533 
Positive ratio  0.880  0.840  0.360  0.680  0.720  0.440  0.720  0.600  0.880  0.720  0.840 
Mean cids  0.527  0.516  0.474  0.523  0.534  0.498  0.031  0.462  0.026  0.422  0.515 
Positive ratio  0.870  0.783  0.522  0.696  0.696  0.435  0.696  0.391  0.652  0.522  0.783 
multiple_relations_table()
is a method that compares multiple signalreturn 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
summary tables
, but the rows display different signals from the list of signals specified under
SignalReturnsRelations()
sigs list. The row names indicate the frequency (‘D,’ ‘W,’ ‘M,’ ‘Q,’ ‘A’) followed by the signal’s and return’s names.
srr.multiple_relations_table().astype("float").round(3)
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

M: COCRR_NSA/last => COXR_VT10  0.516  0.518  0.453  0.521  0.541  0.496  0.001  0.929  0.035  0.0  0.518 
M: COCRR_NSA_ZNI/last => COXR_VT10  0.515  0.518  0.453  0.522  0.541  0.494  0.034  0.007  0.031  0.0  0.518 
M: COCRR_SA/last => COXR_VT10  0.527  0.528  0.468  0.521  0.551  0.506  0.007  0.591  0.044  0.0  0.528 
M: COCRR_SAVT10/last => COXR_VT10  0.527  0.528  0.468  0.521  0.551  0.506  0.019  0.130  0.046  0.0  0.528 
M: COCRR_SAVT10_ZNI/last => COXR_VT10  0.526  0.528  0.469  0.522  0.551  0.505  0.045  0.000  0.041  0.0  0.528 
M: COCRR_SA_ZNI/last => COXR_VT10  0.526  0.528  0.469  0.522  0.551  0.505  0.045  0.000  0.039  0.0  0.528 
M: COCRR_VT10/last => COXR_VT10  0.516  0.518  0.453  0.521  0.541  0.496  0.011  0.385  0.037  0.0  0.518 
M: COCRR_VT10_ZNI/last => COXR_VT10  0.515  0.518  0.453  0.522  0.541  0.494  0.035  0.005  0.033  0.0  0.518 
The
accuracy_bars
method shows the accuracy and balanced accuracy of the predicted relationship for the main signal (
"COCRR_SAVT10_ZNI"
). This can be displayed either by crosssection or by year
srr.accuracy_bars(
type="cross_section",
title="Accuracy of monthly return prediction of the fully adjusted commodity carry, by market",
size=(16, 6),
)
srr.accuracy_bars(
type="years",
title="Accuracy of monthly return prediction of fully adjusted commodity carry, by year",
size=(16, 6),
)
Naive PnL #
We calculate stylized PnLs, i.e., dollarbased profit and loss developments over and above funding costs, according to standard rules applied in previous posts. This is done with
macrosynergy
’s custom class
NaivePnL
. Upon instantiation, we define the list of signals, target variable (
COXR_VT10
), list of crosssections (all available in our case) and (optionally) benchmark (we choose here
USD_EQXR_NSA
, USD equity index futures returns and
GLB_DRBXR_NSA
 Directional risk basket returns)
Positions are recalculated monthly at the end of the week and rebalanced in the following with a oneday slippage for trading time. The longterm volatility of the PnL for positions across all currency areas has been set to 10% annualized. This is no proper voltargeting but mainly a scaling that makes it easy to compare different types of PnLs in graphs.
dix = dict_dir
targ = dix["targ"]
pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigs,
cids=cids,
start="20000101",
bms=["USD_EQXR_NSA", "GLB_DRBXR_NSA"],
)
for sig in sigs:
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",
)
pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls"] = pnl
Here we simply plot directional naive PnL alongside long only PnL:
dix = dict_dir
start = dix["start"]
cidx = dix["cidx"]
sig = [dix["sigs"]]
pnls = [sigs[0] + "_PZN"] + ["Long only"]
dict_labels = {"COCRR_SAVT10_ZNI_PZN": "Fully adjusted carry signal",
"Long only": "Long only, risk parity portfolio"}
pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Naive commodity futures PnL across 23 markets",
xcat_labels=dict_labels,
figsize=(16, 8),
)
To compare PnLs generated by all signals we define a simple dictionary assigning more readable names to the technical tickers:
dict_pnl = {
"COCRR_SAVT10_ZNI_PZN": "Fully adjusted carry",
"COCRR_SAVT10_PZN": "Seasonallyadjusted and voltargeted real carry",
"COCRR_VT10_ZNI_PZN": "Voltargeted, normalized and winsorized real carry",
"COCRR_VT10_PZN": "Voltargeted real carry",
"COCRR_SA_ZNI_PZN": "Seasonallyadjusted, normalized and winsorized real carry",
"COCRR_SA_PZN": "Seasonallyadjusted real carry",
"COCRR_NSA_ZNI_PZN": "Normalized and winsorized real carry",
"COCRR_NSA_PZN": "Real carry",
"Long only": "Long only, risk parity portfolio",
}
dix = dict_dir
start = dix["start"]
cidx = dix["cidx"]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigs]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Commodity futures naive PnLs for different versions of directional carry",
figsize=(16, 8),
xcat_labels=[dict_pnl[k] for k in pnls],
)
The summary of key performance metrics for signals alongside “long only” PnL can be displayed with the method
evaluate_pnls()
, which returns a small dataframe of key PnL statistics.
dix = dict_dir
pnl = dix["pnls"]
pnls = [sig + type for sig in sigs for type in ["_PZN"]] + ["Long only"]
df_eval = pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
)
display(df_eval.transpose().astype("float").round(3))
Return (pct ar)  St. Dev. (pct ar)  Sharpe Ratio  Sortino Ratio  Max 21day draw  Max 6month draw  USD_EQXR_NSA correl  GLB_DRBXR_NSA correl  Traded Months  

xcat  
COCRR_NSA_PZN  3.372  10.0  0.337  0.471  15.558  17.855  0.078  0.030  291.0 
COCRR_NSA_ZNI_PZN  2.637  10.0  0.264  0.367  20.096  26.103  0.050  0.001  291.0 
COCRR_SAVT10_PZN  5.127  10.0  0.513  0.723  14.850  23.579  0.035  0.008  291.0 
COCRR_SAVT10_ZNI_PZN  4.172  10.0  0.417  0.586  16.579  24.183  0.017  0.030  291.0 
COCRR_SA_PZN  4.991  10.0  0.499  0.704  14.578  20.596  0.052  0.003  291.0 
COCRR_SA_ZNI_PZN  3.840  10.0  0.384  0.539  18.484  25.676  0.027  0.021  291.0 
COCRR_VT10_PZN  3.585  10.0  0.359  0.501  14.248  22.505  0.063  0.026  291.0 
COCRR_VT10_ZNI_PZN  2.856  10.0  0.286  0.399  18.248  24.575  0.041  0.004  291.0 
Long only  3.548  10.0  0.355  0.490  20.316  31.813  0.290  0.439  291.0 
Naive longbiased PnL #
In this example, we add one standard deviation to the normalized carry signals of the naïve PnL generator, creating a longbiased strategy (this is done with the optional parameter
sig_add
, which adds a constant to the signal after initial transformation). This allows to give PnLs a long (if a constant is positive) or short bias (negative constant) relative to the signal score. We add here
sig_add=1
to indicate one standard deviation.
dix = dict_dir
targ = dix["targ"]
cidx = dix["cidx"]
pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigs,
cids=cidx,
start="20000101",
bms=["USD_EQXR_NSA", "GLB_DRBXR_NSA"],
)
for sig in sigs:
pnl.make_pnl(
sig,
sig_neg=False,
sig_op="zn_score_pan",
sig_add=1, # add a constant to the signal after the initial transformation. This allows to give PnLs a long or short bias relative to the signal score
thresh=2,
rebal_freq="monthly",
vol_scale=10,
rebal_slip=1,
pnl_name=sig + "_PZN",
)
pnl.make_long_pnl(vol_scale=10, label="Long only")
dix["pnls_long"] = pnl
dix = dict_dir
start = dix["start"]
cidx = dix["cidx"]
pnl = dix["pnls_long"]
pnls = [s + "_PZN" for s in sigs] + ["Long only"]
pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start=start,
title="Commodity futures longbiased naive PnLs for different versions of directional carry",
figsize=(16, 8),
xcat_labels=[dict_pnl[k] for k in pnls],
)
The summary of key performance metrics for signals alongside “long only” PnL can be displayed with the method
evaluate_pnls()
, which returns a small dataframe of key PnL statistics.
dix = dict_dir
pnl = dix["pnls_long"]
pnls = [sig + type for sig in sigs for type in ["_PZN"]] + ["Long only"]
df_eval = pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
)
display(df_eval.transpose().astype("float").round(3))
Return (pct ar)  St. Dev. (pct ar)  Sharpe Ratio  Sortino Ratio  Max 21day draw  Max 6month draw  USD_EQXR_NSA correl  GLB_DRBXR_NSA correl  Traded Months  

xcat  
COCRR_NSA_PZN  4.577  10.0  0.458  0.630  20.991  26.165  0.224  0.384  291.0 
COCRR_NSA_ZNI_PZN  4.187  10.0  0.419  0.572  21.802  27.316  0.211  0.362  291.0 
COCRR_SAVT10_PZN  5.533  10.0  0.553  0.765  24.379  27.912  0.232  0.384  291.0 
COCRR_SAVT10_ZNI_PZN  4.998  10.0  0.500  0.686  24.728  28.743  0.222  0.367  291.0 
COCRR_SA_PZN  5.252  10.0  0.525  0.726  24.464  27.093  0.228  0.387  291.0 
COCRR_SA_ZNI_PZN  4.808  10.0  0.481  0.659  24.759  27.597  0.216  0.364  291.0 
COCRR_VT10_PZN  4.831  10.0  0.483  0.666  20.753  26.592  0.228  0.381  291.0 
COCRR_VT10_ZNI_PZN  4.380  10.0  0.438  0.599  21.174  26.819  0.217  0.363  291.0 
Long only  3.548  10.0  0.355  0.490  20.316  31.813  0.290  0.439  291.0 
Relative to global commodity basket #
This part investigates the second type of trading strategy based on carry: relative carry values to the global commodity basket. For relative signals, we can reasonably only consider voltargeted carry signals for the sake of making crosscommodity signal positions comparable. As a target we look at relative, voladjusted commodity future return
COXR_VT10vGCO
Specs and panel test #
sigs = [cr + "vGCO" for cr in sigs if "VT10" in cr]
targ = "COXR_VT10vGCO"
cidx = cids
start = "20000101"
dict_rel = {
"sig": sigs,
"targ": targ,
"cidx": cidx,
"start": start,
"black": None,
"srr": None,
"pnls": None,
}
dix = dict_rel
sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
cr = msp.CategoryRelations(
dfx,
xcats=[sigs[0], targ],
cids=cidx,
freq="M",
lag=1,
xcat_aggs=["last", "sum"],
fwin=1,
start=dix["start"],
)
cr.reg_scatter(
labels=False,
coef_box="lower right",
title = "Seasonallyadjusted voltargeted carry and subsequent futures returns, relative to the entire commodity basket",
xlab="Seasonallyadjusted voltargeted carry, normalized and winsorized, relative to entire commodity set",
ylab="Next month's voltargeted futures return against global commodity basket",
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_rel
sig = dix["sig"]
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=sigs,
rets=targ,
freqs="M",
start="20000101",
)
dix["srr"] = srr
As for the outright strategy, we use tables to compare basic statistics among different signals. The
.summary_table()
of the SignalReturnRelations class gives a highlevel snapshot of the strength and stability of the main signal relation (seasonally adjusted relative voltargeted carry, normalized and windorized
'COCRR_SAVT10_ZNIvGCO'
) and the target is relative commodity basket return
COXR_VT10vGCO
.
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  

Panel  0.535  0.535  0.460  0.490  0.527  0.542  0.050  0.000  0.045  0.000  0.535 
Mean years  0.538  0.536  0.460  0.488  0.527  0.546  0.055  0.360  0.047  0.320  0.536 
Positive ratio  0.840  0.840  0.120  0.360  0.760  0.880  0.840  0.560  0.840  0.680  0.840 
Mean cids  0.536  0.523  0.465  0.491  0.509  0.537  0.034  0.439  0.029  0.408  0.523 
Positive ratio  0.870  0.696  0.522  0.391  0.609  0.783  0.609  0.435  0.696  0.478  0.696 
To compare different signals in one table we use
multiple_relations_table()
. The method returns a table with standard columns used for
summary tables
, but the rows display different signals from the list of signals specified under
SignalReturnsRelations()
sigs list. The row names indicate the frequency (‘D,’ ‘W,’ ‘M,’ ‘Q,’ ‘A’) followed by the signal’s and return’s names.
srr.multiple_relations_table().astype("float").round(3)
accuracy  bal_accuracy  pos_sigr  pos_retr  pos_prec  neg_prec  pearson  pearson_pval  kendall  kendall_pval  auc  

M: COCRR_SAVT10_ZNIvGCO/last => COXR_VT10vGCO  0.535  0.535  0.460  0.490  0.527  0.542  0.050  0.000  0.045  0.0  0.535 
M: COCRR_SAVT10vGCO/last => COXR_VT10vGCO  0.536  0.535  0.456  0.489  0.528  0.543  0.024  0.052  0.054  0.0  0.535 
M: COCRR_VT10_ZNIvGCO/last => COXR_VT10vGCO  0.521  0.520  0.452  0.490  0.512  0.528  0.039  0.002  0.033  0.0  0.520 
M: COCRR_VT10vGCO/last => COXR_VT10vGCO  0.529  0.528  0.448  0.489  0.520  0.536  0.016  0.188  0.039  0.0  0.527 
The
accuracy_bars
method shows accuracy and balanced accuracy of the predicted relationship for the main signal (
"COCRR_SAVT10_ZNIvGCO"
). This can be displayed either by crosssection or by year.
srr.accuracy_bars(
type="cross_section",
title="Accuracy of monthly return prediction of fully adjusted commodity carry, by commodity",
size=(16, 6),
)
srr.accuracy_bars(
type="years",
title="Accuracy of relative monthly return prediction of the fully adjusted commodity carry, by year",
size=(16, 6),
)
The method
.correlation_bars()
visualizes positive correlation probabilities based on parametric (Pearson) and nonparametric (Kendall) correlation statistics and compares signals between each other, across countries, or years.
srr.correlation_bars(
type="years",
title="Significance of positive correlation of relative fully adjusted commodity carry and subsequent monthly returns, 23 markets",
size=(16, 6),
)
Naive PnL #
We calculate stylized PnLs, i.e., dollarbased profit and loss developments over and above funding costs, according to standard rules applied in previous posts. This is done with
macrosynergy
’s custom class
NaivePnL
. Upon instantiation, we define the list of signals, target variable (
COXR_VT10vGCO
), list of crosssections (all available in our case) and (optionally) benchmark (we choose here
USD_EQXR_NSA
, USD equity index futures returns and
GLB_DRBXR_NSA
 Directional risk basket returns)
Positions are recalculated monthly at the end of the week and rebalanced in the following with a oneday slippage for trading time. The longterm volatility of the PnL for positions across all currency areas has been set to 10% annualized. This is no proper voltargeting but mainly a scaling that makes it easy to compare different types of PnLs in graphs.
dix = dict_rel
targ = dix["targ"]
cidx = dix["cidx"]
pnl = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigs,
cids=cidx,
start="20000101",
bms=["USD_EQXR_NSA", "GLB_DRBXR_NSA"],
)
for sig in sigs:
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"] = pnl
dict_pnr = {
"COCRR_SAVT10_ZNIvGCO_PZN": "Fully adjusted carry",
"COCRR_SAVT10vGCO_PZN": "Seasonallyadjusted and voltargeted real carry",
"COCRR_VT10_ZNIvGCO_PZN": "Voltargeted, normalized and winsorized real carry",
"COCRR_VT10vGCO_PZN": "Voltargeted, norma;lized and winsorized real carry",
}
We utilize the
.plot_pnls()
method to straightforwardly visualize the naive Profit and Loss (PnLs) for all chosen signals.
dix = dict_rel
start = dix["start"]
cidx = dix["cidx"]
naive_pnl = dix["pnls"]
pnls = [s + "_PZN" for s in sigs]
naive_pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
title="Commodity futures naive relative value PnLs for versions of relative carry",
figsize=(16, 8),
xcat_labels=[dict_pnr[k] for k in pnls],
)
The summary of key performance metrics for signals alongside “long only” PnL can be displayed with the method
evaluate_pnls()
, which returns a small dataframe of key PnL statistics.
dix = dict_rel
pnl = dix["pnls"]
pnls = [sig + type for sig in sigs for type in ["_PZN"]]
df_eval = pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
)
display(df_eval.transpose())
Return (pct ar)  St. Dev. (pct ar)  Sharpe Ratio  Sortino Ratio  Max 21day draw  Max 6month draw  USD_EQXR_NSA correl  GLB_DRBXR_NSA correl  Traded Months  

xcat  
COCRR_SAVT10_ZNIvGCO_PZN  6.034896  10.0  0.60349  0.863208  13.089743  22.276771  0.017338  0.033533  291 
COCRR_SAVT10vGCO_PZN  7.30108  10.0  0.730108  1.035271  13.61194  24.635466  0.009876  0.066785  291 
COCRR_VT10_ZNIvGCO_PZN  4.29287  10.0  0.429287  0.610756  13.534062  22.508028  0.028148  0.025306  291 
COCRR_VT10vGCO_PZN  5.003936  10.0  0.500394  0.703539  14.056721  25.846821  0.003532  0.058945  291 
Relative to specific commodity group baskets #
An alternative to global relative positioning is intragroup relative positioning. As distinct groups, we choose here

precious metals (gold, silver, palladium, and platinum),

base metals (aluminum, copper, lead, nickel, tin, and zinc),

fuels (Brent, WTI, natural gas, gasoline, and heating oil),

U.S. corn belt crops (cotton, corn, soy, and wheat), and

other agricultural commodities (coffee, sugar, orange juice, and lumber).
The tested proposition is the predictive power of intragroup relative carry for intragroup relative future return.
Specs and panel test #
We establish a dictionary encompassing a list of potential signals for initial data analysis and ease of use. The primary signal under consideration is
'COCRR_SAVT10_ZNIvRCO'
, which represents normalized relative real seasonallyadjusted futures carry with volatility targeting. The targeted return here is
COXR_VT10vRCO
, representing intragroup relative commodity future returns with volatility targeting.
sigs = [cr + "vRCO" for cr in sigs if "VT10" in cr]
targ = "COXR_VT10vRCO"
cidx = cids
start = "20000101"
dict_reg = {
"sigs": sigs,
"targ": targ,
"cidx": cidx,
"start": start,
"black": None,
"srr": None,
"pnls": None,
}
Accuracy and correlation check #
As with the previous strategies we use
SignalReturnRelations
class from the macrosynergy.signal module together with
.summary_table()
,
.accuracy_bars()
, and
.correlation_bars()
to assess and compare strength and consistency of selected relative signals.
sigs = [
"COCRR_SAVT10_ZNIvRCO",
"COCRR_SAVT10vRCO",
"COCRR_VT10vRCO",
"COCRR_VT10_ZNIvRCO",
]
dix = dict_reg
targ = dix["targ"]
cidx = dix["cidx"]
srr = mss.SignalReturnRelations(
dfx,
cids=cidx,
sigs=sigs,
rets=targ,
freqs="M",
start="20000101",
)
dix["srr"] = srr
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  

Panel  0.525  0.524  0.471  0.495  0.521  0.528  0.063  0.000  0.046  0.000  0.524 
Mean years  0.522  0.522  0.470  0.495  0.519  0.524  0.059  0.267  0.041  0.282  0.521 
Positive ratio  0.720  0.720  0.200  0.400  0.680  0.760  0.800  0.720  0.760  0.680  0.720 
Mean cids  0.524  0.509  0.476  0.497  0.506  0.512  0.038  0.348  0.022  0.391  0.510 
Positive ratio  0.783  0.609  0.391  0.435  0.522  0.652  0.652  0.435  0.652  0.435  0.609 
srr.accuracy_bars(
type="cross_section",
title="Accuracy of monthly return prediction of fully adjusted commodity carry, by commodity",
size=(16, 6),
)
srr.accuracy_bars(
type="years",
title="Accuracy of relative monthly return prediction of the fully adjusted commodity carry, by year",
size=(16, 6),
)
srr.correlation_bars(
type="years",
title="Significance of positive correlation of fully adjusted carry and subsequent relative sector monthly returns, 23 markets",
size=(16, 6),
)
Naive PnL #
As with directional strategy the custom class
NaivePnL
is used to construct naive PnL. Upon instantiation, we define the list of relative carry as potential signals, target variable (
COXR_VT10vRCO
), list of crosssections (all available in our case), and (optionally) benchmark (we choose here
USD_EQXR_NSA
, USD equity index futures returns and
GLB_DRBXR_NSA
 Directional risk basket returns)
dix = dict_reg
targ = dix["targ"]
cidx = dix["cidx"]
pnl_relgroup = msn.NaivePnL(
dfx,
ret=targ,
sigs=sigs,
cids=cidx,
start="20000101",
bms=["USD_EQXR_NSA", "GLB_DRBXR_NSA"],
)
for sig in sigs:
pnl_relgroup.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"] = pnl_relgroup
dict_pns = {
"COCRR_SAVT10_ZNIvRCO_PZN": "Fully adjusted carry",
"COCRR_SAVT10vRCO_PZN": "Seasonallyadjusted and voltargeted real carry",
"COCRR_VT10_ZNIvRCO_PZN": "Voltargeted, normalized and winsorized real carry",
"COCRR_VT10vRCO_PZN": "Voltargeted real carry",
}
dix = dict_reg
start = dix["start"]
cidx = dix["cidx"]
pnl = dix["pnls"]
naive_pnl_relgroup = dix["pnls"]
pnls = [s + "_PZN" for s in sigs]
pnl.plot_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
title="Commodity futures naive relative value PnLs for relative intragroup carry",
figsize=(16, 8),
xcat_labels=[dict_pns[k] for k in pnls],
)
The evaluate_pnls() method facilitates the presentation of key performance metrics for signals and, if required, the longonly Profit and Loss (PnL). When invoked, this method generates a concise dataframe containing essential PnL statistics. For further details, you can refer to the documentation for
evaluate_pnls()
dix = dict_reg
pnl = dix["pnls"]
pnls = [sig + type for sig in sigs for type in ["_PZN"]]
df_eval = pnl.evaluate_pnls(
pnl_cats=pnls,
pnl_cids=["ALL"],
start="20000101",
)
display(df_eval.transpose().astype("float").round(3))
Return (pct ar)  St. Dev. (pct ar)  Sharpe Ratio  Sortino Ratio  Max 21day draw  Max 6month draw  USD_EQXR_NSA correl  GLB_DRBXR_NSA correl  Traded Months  

xcat  
COCRR_SAVT10_ZNIvRCO_PZN  6.300  10.0  0.630  0.932  14.145  15.604  0.014  0.048  291.0 
COCRR_SAVT10vRCO_PZN  7.759  10.0  0.776  1.129  13.519  16.895  0.047  0.094  291.0 
COCRR_VT10_ZNIvRCO_PZN  4.422  10.0  0.442  0.649  11.403  14.722  0.008  0.040  291.0 
COCRR_VT10vRCO_PZN  4.424  10.0  0.442  0.632  10.420  16.573  0.045  0.083  291.0 