Macro Regime 3-Layer Detector
Classify the US equity market regime as BULL / SIDEWAYS / BEAR using a 2-of-3 majority vote across three independent, mathematically uncorrelated signals.
Most regime detectors collapse to a single signal (e.g. "is index above its MA200?"). That makes them brittle — a single bad data point flips the regime. A three-signal vote is more robust: a regime call only changes when at least two layers agree.
Note: Not financial advice. Historical backtests do not guarantee future results.
The three layers
| Layer | Signal | Bull vote when | Bear vote when | Sideways vote when |
|---|---|---|---|---|
| 1. Trend | QQQ ADX(14) + DI± | ADX ≥ 25 and DI+ > DI- | ADX ≥ 25 and DI+ < DI- | ADX < 20 |
| 2. Breadth | % of S&P 500 above MA200 | breadth > 60% | breadth < 40% | 40% ≤ breadth ≤ 60% |
| 3. Volatility | VIX / 20-day realized vol of S&P 500 | ratio outside [0.8, 1.2] and QQQ > MA200 | ratio outside [0.8, 1.2] and QQQ ≤ MA200 | 0.8 ≤ ratio ≤ 1.2 |
Each layer votes independently. Final regime = 2-of-3 majority. Sideways takes priority on tie (two sideways votes → SIDEWAYS overall).
Why three signals
- ADX measures trend strength but not direction reliability when weak.
- Breadth measures whether the rally is participatory or led by a few mega-caps.
- VIX/RV measures whether implied fear matches realized turbulence — divergence often precedes regime shifts.
Any single signal can mislead. Two agreeing signals is much harder to fake.
Step 1: Ensure dependencies
import subprocess, sys
for pkg in ("yfinance", "pandas", "numpy"):
try:
__import__(pkg)
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg])
Step 2: Run the detector
from macro_regime import detect_market_regime
result = detect_market_regime()
# {
# "regime": "BULL" | "SIDEWAYS" | "BEAR",
# "votes": ["BULL", "BULL", "SIDEWAYS"],
# "layers": {
# "1_trend": {"vote": "BULL", "adx": 28.4, "plus_di": 24.1, "minus_di": 18.3},
# "2_breadth": {"vote": "BULL", "breadth_pct": 64.2},
# "3_volatility": {"vote": "SIDEWAYS", "vix": 17.2, "realized_vol": 18.1, "ratio": 0.95},
# },
# "context": {"qqq_close": 540.1, "qqq_ma200": 510.4, "qqq_above_ma200": True}
# }
Default fetches:
- QQQ + ^VIX + S&P 500 sample (50 large caps for breadth — fast, ~10–20 seconds).
- For higher accuracy use
detect_market_regime(use_full_sp500=True)— downloads the full S&P 500 universe (~3–5 minutes).
Step 3: Use the result as a STRATEGY ROUTER (not a gate)
The naive use — "only trade longs in BULL regimes, stay flat in BEAR" — turns out to destroy mean-reversion edge. An 8-year, 843-signal backtest shows BEAR-regime mean-reversion signals actually outperform BULL-regime ones:
| Regime | Mean 5d | Win rate (5d) |
|---|---|---|
| BULL | +0.80% | 55.5% |
| SIDEWAYS | +2.07% | 67.7% |
| BEAR | +2.26% | 65.6% |
So treat the regime as a router, not a gate:
result = detect_market_regime()
regime = result["regime"]
if regime == "BULL":
enable_momentum_breakout() # trend-following needs a trending tape
elif regime == "SIDEWAYS":
enable_mean_reversion() # chop is mean-reversion's home turf
else: # BEAR
enable_mean_reversion() # deepest oversold = strongest snap-back
# Staying flat in BEAR is a risk-tolerance choice, not a signal-quality one.
See README.md for the full backtest table and reasoning.
Step 4: Respond to the user
Present the breakdown so the user understands why the regime is what it is. Don't just say "BULL" — show which layers agreed.
Current regime: BULL (2-of-3 vote)
Layer Signal Vote 1. Trend (ADX 28.4 / DI+ 24.1 > DI- 18.3) Strong uptrend 🟢 BULL 2. Breadth (64.2% of S&P 500 above MA200) Broad participation 🟢 BULL 3. Volatility (VIX 17.2 / RV 18.1 = 0.95) Implied ≈ realized 🟡 SIDEWAYS Layer 3 disagrees but the trend + breadth majority drives the call. Momentum strategies are currently allowed.
Caveats to mention
- This is a macro filter, not a single-stock signal. It tells you the environment, not what to buy.
- Thresholds (25/20 for ADX, 60/40 for breadth, 0.8/1.2 for VIX/RV) are calibrated for US equities and may need adjustment for other markets.
- The sample-of-50 breadth is a reasonable proxy; full S&P 500 is more accurate but slower.
Real-world example
This skill was extracted from the quant-scanner project where it is the top-level macro gate: BULL allows mean-reversion + momentum, SIDEWAYS allows mean-reversion only, BEAR blocks all new long entries. Combined with the vix-regime skill for VIX-band overlay, it forms a two-stage macro filter.