Squeeze & Exhaustion Breadth

Cross-market tactical signal tracking how widespread short-squeeze and exhaustion risks are across the futures universe, based on CoT-derived squeeze_risk and exhaustion_risk flags.

Abstract

This signal measures how broadly squeeze and exhaustion conditions appear across futures markets in the CoT dataset. For each report date, it aggregates market-level tactical flags (squeeze_risk, exhaustion_risk) into breadth measures, normalises them with a robust z-score, and maps them into simple regimes such as High Squeeze Risk or High Exhaustion Risk. The result is a compact indicator of whether the futures complex is vulnerable to forced covering or trend fatigue.

1. Inputs

2. Daily Aggregation

For each CoT report date, the model aggregates tactical flags into cross-market breadth metrics:

These raw counts are then converted into shares to make the signal comparable over time:

squeeze\_share = squeeze\_count / n\_markets
exhaustion\_share = exhaustion\_count / n\_markets

If n_markets = 0 on any date, it is replaced with NaN to avoid division by zero.

3. Normalised Signal Construction

To stabilise the breadth measures and place them on a comparable scale, a robust z-score transformation is applied separately to squeeze_share and exhaustion_share:

z\_{rob}(X) = \dfrac{X - median(X)}{1.4826 \cdot MAD(X)}

Here, MAD(X) is the median absolute deviation. Using median/MAD instead of mean/standard deviation makes the signal more resilient to outliers and structural breaks in the CoT sample.

4. Regime Labelling

The normalised values are mapped into intuitive risk regimes using symmetric thresholds:

z \ge 1.0 \Rightarrow \textit{High Squeeze Risk}
0.5 \le z < 1.0 \Rightarrow \textit{Moderate Squeeze Risk}
z < 0.5 \Rightarrow \textit{Normal}

The same thresholds are applied to the exhaustion z-score to define High Exhaustion Risk, Moderate Exhaustion Risk, and Normal exhaustion conditions.

5. Implementation (Python)

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

# Ensure flags exist
for flag in ["squeeze_risk", "exhaustion_risk", "crowded_long", "crowded_short"]:
    if flag not in df_se.columns:
        df_se[flag] = False

# Aggregation
df_squeeze_exhaust = (
    df_se.groupby("Report_Date").agg(
        n_markets=("Market_and_Exchange_Names", "nunique"),
        squeeze_count=("squeeze_risk", "sum"),
        exhaustion_count=("exhaustion_risk", "sum"),
        crowded_long_count=("crowded_long", "sum"),
        crowded_short_count=("crowded_short", "sum"),
    ).reset_index()
)

df_squeeze_exhaust["n_markets"] = df_squeeze_exhaust["n_markets"].replace(0, np.nan)

df_squeeze_exhaust["squeeze_share"] = (
    df_squeeze_exhaust["squeeze_count"] / df_squeeze_exhaust["n_markets"]
)

df_squeeze_exhaust["exhaustion_share"] = (
    df_squeeze_exhaust["exhaustion_count"] / df_squeeze_exhaust["n_markets"]
)

# Robust z-score helper
def _robust_z(series: pd.Series) -> pd.Series:
    x = pd.to_numeric(series, errors="coerce").astype(float)
    med = x.median()
    mad = (x - med).abs().median()
    if mad == 0 or np.isnan(mad):
        return pd.Series(np.nan, index=series.index)
    return (x - med) / (1.4826 * mad)

# Normalised signals
df_squeeze_exhaust["Squeeze_z"] = _robust_z(df_squeeze_exhaust["squeeze_share"])
    
df_squeeze_exhaust["Exhaustion_z"] = _robust_z(df_squeeze_exhaust["exhaustion_share"])

# Regime labels
def _squeeze_label(z: float) -> str:
    if pd.isna(z):
        return "Unknown"
    if z >= 1.0:
        return "High Squeeze Risk"
    if 0.5 <= z < 1.0:
        return "Moderate Squeeze Risk"
    return "Normal"


def _exhaust_label(z: float) -> str:
    if pd.isna(z):
        return "Unknown"
    if z >= 1.0:
        return "High Exhaustion Risk"
    if 0.5 <= z < 1.0:
        return "Moderate Exhaustion Risk"
    return "Normal"


df_squeeze_exhaust["Squeeze_Regime"] = df_squeeze_exhaust["Squeeze_z"].apply(_squeeze_label)

df_squeeze_exhaust["Exhaustion_Regime"] = df_squeeze_exhaust["Exhaustion_z"].apply(_exhaust_label)

6. Interpretation

7. Limitations

8. Practical Use Cases