CoT Market Tone

A macro-level breadth and sentiment indicator derived from speculative crowding across all futures markets in the CoT dataset. Measures systemic Risk-On vs Risk-Off tone using crowding breadth, net tone, and robust z-score normalisation.

Abstract

The CoT Market Tone signal aggregates daily positioning across all markets to determine whether speculative futures activity is broadly Risk-On or Risk-Off. By comparing crowded-long vs crowded-short breadth, constructing a net tone index, and applying robust z-score scaling, the model classifies market tone into regimes ranging from Strong Risk-On to Strong Risk-Off.

1. Inputs

2. Daily Aggregation: Breadth Metrics

For each CoT reporting date, the model aggregates market-level information to compute Risk-On vs Risk-Off breadth:

These are used to compute:

crowded\_long\_share = crowded\_long\_count / n\_markets
crowded\_short\_share = crowded\_short\_count / n\_markets
net\_tone = crowded\_long\_share − crowded\_short\_share

3. Robust Z-Score Normalisation

A robust z-score is applied to the daily net tone to stabilise the signal and reduce sensitivity to outliers:

z\_{rob}(X) = \dfrac{X − median(X)}{1.4826 · MAD(X)}

This transformation ensures that extreme values, skewed distributions, and structural shifts do not distort the market tone classification.

4. Regime Classification

The robust z-score is mapped to qualitative market regimes:

z ≥ 1.0 → Strong Risk-On
0.5 ≤ z < 1.0 → Risk-On
−0.5 < z < 0.5 → Neutral
−1.0 < z ≤ −0.5 → Risk-Off
z ≤ −1.0 → Strong Risk-Off

5. Implementation (Python)

df_tone = df_cot_signals.copy()
df_tone["Report_Date"] = pd.to_datetime(df_tone["Report_Date"], errors="coerce")

# Aggregation
df_cot_tone_daily = (
    df_tone.groupby("Report_Date").agg(
        n_markets=("Market_and_Exchange_Names", "nunique"),
        crowded_long_count=("crowded_long", "sum"),
        crowded_short_count=("crowded_short", "sum"),
    ).reset_index()
)

df_cot_tone_daily["crowded_long_share"] = df_cot_tone_daily["crowded_long_count"] / df_cot_tone_daily["n_markets"]
df_cot_tone_daily["crowded_short_share"] = df_cot_tone_daily["crowded_short_count"] / df_cot_tone_daily["n_markets"]

df_cot_tone_daily["net_tone"] = df_cot_tone_daily["crowded_long_share"] - df_cot_tone_daily["crowded_short_share"]

# Robust z-score
med = df_cot_tone_daily["net_tone"].median()
mad = (df_cot_tone_daily["net_tone"] - med).abs().median()
df_cot_tone_daily["CoT_Market_Tone_z"] = (df_cot_tone_daily["net_tone"] - med) / (1.4826 * mad)

# Regimes
def _tone_label(z):
    if pd.isna(z): return "Unknown"
    if z >= 1.0: return "Strong Risk-On"
    if z >= 0.5: return "Risk-On"
    if z > -0.5: return "Neutral"
    if z > -1.0: return "Risk-Off"
    return "Strong Risk-Off"

df_cot_tone_daily["CoT_Market_Tone_Regime"] = df_cot_tone_daily["CoT_Market_Tone_z"].apply(_tone_label)

6. Interpretation

7. Limitations

8. Practical Use Cases