Vol-clustering breaks the regime filter — but not for the obvious reason
what you'll learn · Why the vol-regime gate fails on the very signal it was designed to detect, and what this tells us about the difference between 'mean-reverting threshold gates' and 'persistent-state classifiers'.
vol_regime_filter was designed for vol-regime detection. Add vol clustering to the synthetic — the regime structure it's allegedly designed to detect — and its Sharpe drops by 1.3 below baseline. The reason isn't that the gate fails to fire; the gate fires CONSTANTLY. When vol clusters, the vol_5/vol_60 ratio is structurally elevated most of the time, so the gate drops most of the universe most of the time, leaving 2-3 names rotating through the ranking on a cluster-driven schedule that has nothing to do with momentum.
XsMomentumWithVolRegimeFilterStrategy (vol_regime_filter)
shipped with the explicit thesis: “drop symbols whose
vol_5/vol_60 ratio > 1.5; those are the regime-transitioning
names and momentum doesn’t work on them” (see
vol-ratio-as-cross-symbol-regime-gate).
On i.i.d. vol data (the existing harness mode), the strategy was the harness’s top mean-Sharpe arm at +1.493 (PR #655).
On clustered-vol data (the new --vol-cluster-strength mode),
the same strategy posts mean −0.265 — a 1.76 drop. Worse than
baseline when applied to data that has exactly the regime
structure the strategy claims to detect.
The 3-seed sample
mean stdev min max Δ vs baseline
baseline +1.048 2.235 -1.364 +3.049 —
three_clock_momentum -0.531 2.023 -2.291 +1.678 −1.579
vol_regime_filter -0.265 2.295 -1.739 +2.380 −1.313
three_clock_vol_regime -0.180 2.418 -2.193 +2.502 −1.228
Every arm except baseline is well below the noise floor; the strategies designed for regime detection are the WORST underperformers. Baseline’s +1.048 mean is intact (i.i.d. or clustered, the noise averages out), but baseline’s stdev exploded to 2.235 (from 0.581 on i.i.d. data).
Why the gate fails
The original thesis assumed vol_5/vol_60 would be a
discriminating signal — sometimes < 1.5 (stable regime,
trade) and sometimes > 1.5 (transition, skip). On i.i.d. vol
data, that’s right: most days have ratio ≈ 1, occasional days
spike above 1.5.
On clustered-vol data, vol_5/vol_60 is structurally
elevated for long stretches:
- A cluster of high-vol days:
vol_5(the short window) is high, butvol_60(the long window) is still averaging in the prior calm period → ratio stays > 1.5. - After the cluster fades:
vol_5drops fast, butvol_60is still elevated → ratio stays low for an extended period.
The ratio is mean-reverting on i.i.d. vol but ARMA-shaped on clustered vol. The gate’s binary “always > 1.5” or “always < 1.5” is the wrong shape for an ARMA signal.
The deeper distinction
This surfaces a useful taxonomy:
- Mean-reverting threshold gates work when the gated signal oscillates around the threshold. The original vol-regime-filter design assumed this.
- Persistent-state classifiers work when the gated signal has long runs above or below the threshold. Hidden Markov Models, regime-switching, or two-state classification applied here.
vol_5/vol_60 on i.i.d. vol is mean-reverting. On clustered
vol it’s a persistent state. The gate that works for the first
case (drop > 1.5) is the wrong tool for the second case (need
to know which regime, not just whether the ratio is currently
elevated).
What this isn’t
-
Not a critique of the original thesis. The thesis (“regime change is a real signal”) is right. The implementation (binary gate on
vol_5/vol_60) only catches it when the underlying signal oscillates around the threshold. The mismatch is between the gate’s binary nature and the data’s persistent character. -
Not a claim that vol-regime detection doesn’t work. It does — but the gate needs to be regime-AWARE, not just threshold-AWARE. The current code mistakes “vol is currently elevated” for “we’re in a regime transition.”
-
Not a 3-seed result you should publish. The numbers are directional, not definitive. A larger run + the analysis script’s hit-rate metric would refine the picture. The qualitative claim (the gate fires too often) is the load-bearing observation; the specific magnitude isn’t.
The fix (out of scope for this PR)
The gate’s discipline rule should be: gate on regime transitions, not on current regime state. Implementation sketch:
# Current: gate on level
if vol_5 / vol_60 > 1.5:
drop_symbol(sym)
# Better: gate on change-in-level
if abs(vol_5_now / vol_5_prev - 1) > 0.5:
drop_symbol(sym) # the regime is changing right now
The first measures persistent state; the second measures
transitions. A future XsMomentumWithVolRegimeTransitionFilterStrategy
would test this — out of scope for this note, but the
research question is named.
The discipline rule
A threshold gate works when the gated signal oscillates around the threshold. When the signal has persistent state (clusters, regimes), the threshold gate is the wrong tool — you need a regime classifier that distinguishes “currently elevated” from “transitioning.” Test the gate’s behaviour on data with the EXACT structure it claims to handle.
A corollary: the vol_regime_filter passing the i.i.d. test (PR #655’s +1.493 result) said “the implementation runs and produces a Sharpe number.” The vol-clustering test (this PR’s result) said “the implementation is wrong for the regime structure it claims to address.” Both are useful; only the second is the load-bearing test.