---
title: "Regime Mixing in Perpetual Futures Signals | 0xArchive"
description: "0xArchive Research explains how regime mixing obscured the conditional relationship between funding divergence and returns across Hyperliquid perpetual markets."
canonical_url: "https://0xarchive.io/blog/regime-mixing-perpetual-futures-signals"
markdown_url: "https://0xarchive.io/blog/regime-mixing-perpetual-futures-signals.md"
route: "/blog/regime-mixing-perpetual-futures-signals"
robots: "index, follow"
generated_from: "prerendered_html"
---

# Regime Mixing in Perpetual Futures Signals | 0xArchive
0xArchive Research explains how regime mixing obscured the conditional relationship between funding divergence and returns across Hyperliquid perpetual markets.
*0xArchive Research · Funding Rate Alpha, Part 2 of 3*

In Part 1 we built a dollar-weighted divergence signal. Pooled across ten symbols, it mildly mean-reverts: `corr(z, fwd_return)` commonly falls between roughly `-0.02` and `-0.12`. Read naively, that is a counter-trend signal.

That reading is an aggregation artifact. Separating the sample by the signs of open-interest change and price change produces a direction-dependent result that the pooled number cannot describe.

- Pooled across ten symbols and the fixed sample, divergence mildly mean-reverts.
- The pooled result mixes rising-price and falling-price states.
- At 72 hours, Q5-minus-Q1 is **+0.3760% in OI-up/price-up** and **-0.6890% in OI-down/price-down** .
- The two rising-price quadrants have positive spreads; the two falling-price quadrants have negative spreads.
- This is a regime-mixing, Simpson-like effect, not a textbook case where every subgroup reverses the pooled sign.

![Four observed open-interest and price quadrants](https://api.0xarchive.io/cms-assets/2026/07/c023ac31-56ef-40d4-a019-fd1e317a7755.webp)

## Simpson's paradox: when the pooled result changes the story

Simpson's paradox occurs when a relationship in combined data weakens, disappears, or reverses after the data is separated into relevant groups. The arithmetic is still correct. The problem is that the pooled average mixes two things:

1. What happens within each group.
2. How much of each group appears in the average.

If the groups have very different baseline outcomes, changing their weights can dominate the relationship we are trying to measure.

The 1973 Berkeley graduate-admissions data is the classic example. The university-wide admission rate appeared to favor men. But applicants were not distributed evenly across departments. Women applied more often to highly selective departments, while men applied more often to departments with higher admission rates. Once the comparison was made within departments, the apparent university-wide disadvantage largely disappeared. The pooled result had mixed department selectivity with applicant gender.

A small hypothetical example makes the mechanism clearer. These numbers are illustrative, not results from our dataset:

| Observed price state | Low divergence return | High divergence return |
| --- | --- | --- |
| Rising price | +0.10% | +0.20% |
| Falling price | -0.60% | -0.50% |

Within both price states, high divergence is followed by a return that is 0.10 percentage points higher than low divergence.

Now change only the composition of the buckets. Suppose 80% of the high-divergence observations come from the falling-price state, while 80% of the low-divergence observations come from the rising-price state. The pooled averages become:

```text
High divergence:
20% × +0.20% + 80% × -0.50% = -0.36%

Low divergence:
80% × +0.10% + 20% × -0.60% = -0.04%
```

The pooled data now says high divergence performs worse, even though high divergence performs better within both states. The result reversed because the high-divergence bucket contained far more observations from the state with lower returns.

That is the danger in our funding-divergence result. A pooled coefficient does not measure one stable market response if the divergence buckets contain different mixtures of rising-price and falling-price observations. It can mostly reflect which market states ended up in each bucket.

Our result is Simpson-like rather than a strict textbook case. The two falling-price quadrants retain the pooled negative sign, so every subgroup does not reverse. But the central lesson still applies: once we condition on the observed price and open-interest state, the signal does not have one universal meaning.

## The four observed quadrants

For each `(t, W)` observation, we use two quantities from the open-interest endpoint: the change in OI over window `W`, and the change in mark price over the same window. Their signs partition the observation into four quadrants.

| OI delta | price delta | observed quadrant | economic shorthand |
| --- | --- | --- | --- |
| Up | Up | OI-up, price-up | consistent with new-long activity, but not identified |
| Down | Up | OI-down, price-up | consistent with short covering, but not identified |
| Up | Down | OI-up, price-down | consistent with new-short activity, but not identified |
| Down | Down | OI-down, price-down | consistent with long unwinds, but not identified |

The quadrant is observed. The final column is shorthand, not participant identity. Every matched perpetual position has a long and a short, so OI and price alone cannot tell us who initiated the trade.

No ML, no HMM, no distributional assumption. Just the four corners of an OI/price sign quadrant.

```python
def classify_regime(d_oi: float, d_price: float) -> str:
    # Internal aliases are economic shorthand, not observed participant labels.
    if d_oi > 0 and d_price > 0: return "new_longs"
    elif d_oi < 0 and d_price > 0: return "short_covering"
    elif d_oi > 0 and d_price < 0: return "new_shorts"
    elif d_oi < 0 and d_price < 0: return "long_unwind"
    return "neutral"
```

## The headline finding: the sign follows the price state

The corrected quantile table, averaged across all symbols and `W` values and broken out by quadrant:

| observed quadrant | 1h | 4h | 24h | 72h |
| --- | --- | --- | --- | --- |
| OI-up, price-up | +0.0259% | +0.0529% | +0.1869% | **+0.3760%** |
| OI-down, price-up | +0.0087% | +0.0263% | +0.1205% | +0.2657% |
| OI-up, price-down | -0.0729% | -0.0972% | -0.2909% | -0.3709% |
| OI-down, price-down | -0.0587% | -0.1009% | -0.3275% | **-0.6890%** |

Each cell is the mean Q5-minus-Q1 spread in forward return. Positive means high divergence was followed by a higher return than low divergence in that quadrant. Negative means the opposite.

Within this sample, the sign of the spread matches the sign of the contemporaneous price delta. Rising-price quadrants carry positive spreads; falling-price quadrants carry negative spreads. The pooled negative coefficient does not represent one stable response to divergence.

![Pooled relationship versus quadrant-conditioned spreads](https://api.0xarchive.io/cms-assets/2026/07/e1e1420d-55c9-4893-a9eb-b6783d08dca1.webp)

## Why pooling changes the story

The trap has a precise mechanism. Ask what the pooled return for a divergence bucket actually averages:

```text
E[return | divergence bucket]
  = sum over quadrants q of
    E[return | divergence bucket, q] × P(q | divergence bucket)
```

Both pieces can change. The return relationship inside a quadrant can differ, and the mix of quadrants inside Q1 and Q5 can differ. One pooled coefficient collapses both effects into a single number.

A high-divergence bucket with more falling-price observations is not comparable to a low-divergence bucket with more rising-price observations. The arithmetic can be correct while the trading interpretation is wrong.

## Same `z`, different event

| `z` reading | observed quadrant | historical association | event hypothesis |
| --- | --- | --- | --- |
| `z = +2` | OI-up, price-up | positive spread | test continuation, not a pooled fade |
| `z = +2` | OI-down, price-down | negative spread | test a short event filter |

The numerical reading is the same. The observed state is not.

One interpretation of OI-down, price-down is sustained unwind pressure. One interpretation of OI-up, price-up is continued expansion during a rising market. Those are economic stories, not identified facts about traders. The data supports the conditional return association, not the participant label.

## Why this matters, generally

- **Pooled correlations are cheaper to compute but weaker evidence.** Find a signal that mean-reverts pooled? Ask what subgroup structure is producing it.
- **A simple classifier can still be load-bearing.** The OI-by-price sign quadrant is two columns of arithmetic. It exposes that the same divergence reading can describe different events.
- **Stratification is not permission to search until a sign looks useful.** Define the state variables first, form one rule, then score it chronologically.

That last step matters. The strongest negative 72-hour spread is in OI-down, price-down: `-0.6890%` between Q5 and Q1. It suggests a short event filter when divergence is high. The mirror hypothesis uses a low z-score in OI-up, price-down as a long filter.

**Next:** does that explicit rule survive causal execution timing, hourly funding, transaction costs, and sequential chronological testing?

## Reproduce this analysis

The publication run uses the fixed cutoff `2026-07-10T23:59:59.999Z`. Funding events are reconstructed once per completed hour before the signal is computed. The quadrant classifier and Q5-minus-Q1 calculation are:

```python
def q5_minus_q1_by_regime(data, W, H):
    rows = []
    for symbol, df in data.items():
        for regime in REGIMES:
            mask = df[f"regime_W{W}"] == regime
            pair = pd.DataFrame({
                "z": df.loc[mask, f"z_W{W}"],
                "f": df.loc[mask, f"fwd_{H}h"],
            }).dropna()
            if len(pair) < 50:
                continue
            try:
                bins = pd.qcut(pair["z"], 5, labels=False)
            except ValueError:
                ranked = pair["z"].rank(method="first", pct=True)
                bins = pd.cut(ranked, 5, labels=False)
            means = pair.groupby(bins)["f"].mean()
            if 0 not in means.index or 4 not in means.index:
                continue
            rows.append({
                "symbol": symbol,
                "regime": regime,
                "spread": means.loc[4] - means.loc[0],
            })
    return pd.DataFrame(rows).groupby("regime")["spread"].mean()
```

**Funding Rate Alpha · Part 2 of 3**

[Part 1: Funding Rates as a Positioning Signal](https://0xarchive.io/blog/funding-pressure-map-perpetual-futures)

Next in the series: The Squeeze Signal Survived Five Chronological Tests.
