Build a Stock Scanner Series — Part 4 of 6

Regime Detection — The Scanner That Adapts to the Market

A momentum scanner in a crash is a money furnace. A regime detector analyzes 6 market indicators daily and shifts your strategy weights automatically. Build one from scratch.

5 Market Regimes 6 Weighted Indicators Dynamic Weights Regime Timeline
Build a Stock Scanner That Actually Works4/6
Why Regime Matters5 Market Regimes6 IndicatorsDynamic WeightsImplementationKey Takeaways
Why Regime Matters

Why Static Scanners Blow Up

In March 2020, a momentum scanner would have been screaming "BUY" on hundreds of stocks that had just bounced 5-10% off their lows. RSI was climbing, volume was surging, MACD was crossing up. Every technical signal said "go." The market then proceeded to drop another 20% in the following week.

The problem: the scanner had no concept of regime. It did not know that VIX was at 82, credit spreads were blowing out, and the Fed had just emergency-cut rates to zero. The same signals that work beautifully in a bull market become death traps in a crash.

The Regime Paradox

The same setup (RSI oversold + volume spike + MACD crossover) has a 62% win rate in Risk-On markets and a 28% win rate in Risk-Off markets. Without regime detection, you are averaging those two outcomes and calling it a "strategy." That is like averaging the temperature of Sahara and Antarctica and dressing for 15C.

The 5 Market Regimes

Five Regimes, Five Playbooks

Markets do not exist in a binary "bull" or "bear" state. There are five distinct regimes, each requiring a different scanner configuration:

Risk-On (Score > 0.75)

SPY above rising EMAs, VIX below 18, DXY weak, credit tight. Playbook: Heavy momentum (45%), breakout (30%), contraction (20%), minimal mean-reversion (5%). This is where trend-following prints money.

Recovery (Score 0.60–0.75)

SPY reclaiming EMAs, VIX falling from elevated levels, breadth improving. Playbook: Balanced momentum (30%), breakout (25%), contraction (25%), mean-reversion (20%). The market is healing — look for leaders emerging from the wreckage.

Neutral (Score 0.40–0.60)

Mixed signals. SPY choppy, VIX 18-25, no clear direction. Playbook: Equal weight across all strategies (25% each). Focus on stock-picking over market direction.

Early Risk-Off (Score 0.25–0.40)

SPY breaking below EMA50, VIX rising above 25, credit widening. Playbook: Mean-reversion (40%), contraction (35%), minimal momentum (15%), defensive breakout on hedges (10%). Shift to defensive names and short setups.

Risk-Off (Score < 0.25)

SPY below EMA200, VIX above 30, credit blowout, DXY surging. Playbook: Mean-reversion only on quality names (45%), contraction (40%), short candidates (10%), no momentum (5%). Capital preservation is priority one.

RegimeScore RangeVIX TypicalKey BehaviorScanner Action
Risk-On> 0.75< 18Strong uptrend, low fearAggressive momentum and breakout
Recovery0.60 – 0.7518 – 22Improving breadthBalanced, look for leaders
Neutral0.40 – 0.6018 – 25Choppy, no directionEqual weight, stock-picking
Early Risk-Off0.25 – 0.4025 – 35Deteriorating, cautionDefensive, hedges, oversold quality
Risk-Off< 0.25> 30Crash / panicPreservation, short setups, GLD/TLT
The 6 Indicators

Building the Regime Detector

The regime detector uses 6 indicators, each producing a score from 0.0 (bearish) to 1.0 (bullish). The indicators are weighted and combined into a single regime score.

#IndicatorWeightSignalComputation
1S&P 500 Trend30%EMA(20) vs EMA(50)1.0 if EMA20 > EMA50 by >2%, 0.5 if flat, 0.0 if EMA20 < EMA50 by >2%
2VIX Level (inverted)25%Fear gauge1.0 if VIX < 15, 0.5 if 20-25, 0.0 if > 35
3Dollar DXY (inverted)15%Risk appetite1.0 if DXY falling >1%, 0.5 if flat, 0.0 if surging >1%
4Credit HYG/LQD15%Institutional risk1.0 if ratio rising (risk-on), 0.0 if falling (flight to quality)
5Treasury TLT (inverted)10%Bond vs equity1.0 if TLT falling (money into stocks), 0.0 if surging (flight to safety)
6Bitcoin Momentum5%Liquidity proxy1.0 if BTC EMA20 > EMA50, 0.0 otherwise
Regime Score Formula
regimeScore = 0.30 x SPY + 0.25 x VIX + 0.15 x DXY + 0.15 x HYG + 0.10 x TLT + 0.05 x BTC
Python — Regime Detector
class RegimeDetector:
    """Detect market regime from 6 macro indicators."""

    def __init__(self):
        self.weights = {
            'spy_trend': 0.30,
            'vix_level': 0.25,
            'dxy_trend': 0.15,
            'credit_spread': 0.15,
            'tlt_trend': 0.10,
            'btc_momentum': 0.05,
        }

    def _score_spy(self, spy_data):
        """Score SPY trend: EMA20 vs EMA50."""
        ema20 = spy_data['ema_20']
        ema50 = spy_data['ema_50']
        spread = (ema20 - ema50) / ema50
        if spread > 0.02: return 1.0
        elif spread > 0.005: return 0.75
        elif spread > -0.005: return 0.5
        elif spread > -0.02: return 0.25
        else: return 0.0

    def _score_vix(self, vix):
        """Score VIX (inverted: low VIX = bullish)."""
        if vix < 15: return 1.0
        elif vix < 20: return 0.75
        elif vix < 25: return 0.5
        elif vix < 35: return 0.25
        else: return 0.0

    def detect(self, market_data):
        """Run all 6 indicators. Return regime + score."""
        scores = {
            'spy_trend': self._score_spy(market_data['SPY']),
            'vix_level': self._score_vix(market_data['VIX']),
            'dxy_trend': self._score_dxy(market_data['DXY']),
            'credit_spread': self._score_credit(market_data),
            'tlt_trend': self._score_tlt(market_data['TLT']),
            'btc_momentum': self._score_btc(market_data['BTC']),
        }

        # Weighted composite
        regime_score = sum(
            scores[k] * self.weights[k] for k in scores
        )

        # Map to regime name
        if regime_score > 0.75: regime = 'risk_on'
        elif regime_score > 0.60: regime = 'recovery'
        elif regime_score > 0.40: regime = 'neutral'
        elif regime_score > 0.25: regime = 'early_risk_off'
        else: regime = 'risk_off'

        return {
            'regime': regime,
            'score': round(regime_score, 3),
            'indicators': scores,
        }

Why These 6?

SPY trend (30%) is the king — if the market is going up, most stocks go up. VIX (25%) measures fear intensity. DXY (15%) captures global risk appetite — a strong dollar means money is fleeing to safety. HYG/LQD (15%) is the institutional tell — when institutions dump high-yield bonds for investment-grade, they are positioning for trouble. TLT (10%) is the bond refuge indicator. BTC (5%) is a liquidity barometer — crypto dies first when liquidity dries up.

Dynamic Strategy Weights

Shifting Strategy Weights by Regime

Once you know the regime, you adjust the weight given to each scanner strategy. This is where the real magic happens — the scanner literally reconfigures itself every morning based on what the market is doing.

StrategyRisk-OnRecoveryNeutralEarly Risk-OffRisk-Off
Momentum45%30%25%15%5%
Breakout30%25%25%10%10%
Contraction20%25%25%35%40%
Mean Reversion5%20%25%40%45%
Python — Strategy Weight Mapper
STRATEGY_WEIGHTS = {
    'risk_on':         {'momentum': 45, 'breakout': 30, 'contraction': 20, 'mean_reversion': 5},
    'recovery':        {'momentum': 30, 'breakout': 25, 'contraction': 25, 'mean_reversion': 20},
    'neutral':         {'momentum': 25, 'breakout': 25, 'contraction': 25, 'mean_reversion': 25},
    'early_risk_off': {'momentum': 15, 'breakout': 10, 'contraction': 35, 'mean_reversion': 40},
    'risk_off':        {'momentum': 5,  'breakout': 10, 'contraction': 40, 'mean_reversion': 45},
}

def get_weights(regime):
    """Return strategy weights for detected regime."""
    return STRATEGY_WEIGHTS[regime]

The Feedback Loop

The regime detector runs at the start of each daily scan. The full loop:

  1. Fetch macro data — SPY, VIX, DXY, HYG, LQD, TLT, BTC-USD latest bars
  2. Score each indicator — 0.0 to 1.0
  3. Compute regime score — weighted average
  4. Map to regime — one of 5 regimes
  5. Look up strategy weights — from the weight table
  6. Pass weights to filter + scoring engine — scanner reconfigures
  7. Run scanner — with regime-appropriate behavior
Putting It All Together

The Regime-Aware Scanner

Python — Complete Daily Scan with Regime
def daily_scan(universe_df, market_data):
    """Full daily scan: regime -> filter -> score -> rank."""

    # Step 1: Detect regime
    detector = RegimeDetector()
    regime_result = detector.detect(market_data)
    regime = regime_result['regime']
    print(f"Regime: {regime} (score: {regime_result['score']})")

    # Step 2: Get strategy weights
    weights = get_weights(regime)
    print(f"Weights: {weights}")

    # Step 3: Run filters (from Part 2)
    pipeline = ScannerPipeline()
    candidates = pipeline.run(universe_df, regime)

    # Step 4: Score candidates (from Part 3)
    scorer = ScoringEngine()
    all_scored = []
    for strategy, weight in weights.items():
        strat_candidates = candidates[candidates['strategy'] == strategy]
        scored = scorer.score_candidates(strat_candidates, strategy, weight)
        all_scored.append(scored)

    # Step 5: Merge and rank
    merged = pd.concat(all_scored)
    merged = merged.sort_values('final_score', ascending=False)
    merged = merged.drop_duplicates(subset=['ticker'], keep='first')

    # Step 6: Take top 10 with diversification
    top10 = enforce_diversification(merged.head(20), max_per_sector=2)

    print(f"\nTop 10 Setups ({regime}):")
    for _, row in top10.head(10).iterrows():
        print(f"  {row['ticker']:6s} | {row['strategy']:15s} | Score: {row['final_score']}")

    return top10.head(10)

Anti-Pattern: Regime Whiplash

If the regime score hovers near a boundary (e.g., 0.41 one day, 0.39 the next), your scanner will whip between Neutral and Early Risk-Off strategies daily. To prevent this, add hysteresis: require the score to move at least 0.05 past a boundary before switching regime. Alternatively, use a 3-day exponential average of the regime score instead of the raw daily value.

Knowledge Check

Test Your Understanding

Quiz 1: VIX is at 28 and rising, breadth is at 38%, SPY is below its falling 50-day MA. What regime is the market in?
Answer: Early Risk-Off to Risk-Off. VIX above 25 and rising signals elevated fear. Breadth at 38% means fewer than 4 in 10 stocks are above their 200-day MA — narrow participation consistent with a deteriorating market. SPY below a falling 50-day MA confirms the downtrend. All three indicators align on Risk-Off. The scanner should switch to defensive strategies: scan for shorts, hedges (GLD, TLT), and high-quality dividend payers. Reduce total exposure to 30-50% and tighten stops to 0.75x ATR.
Quiz 2: Why does the regime classifier use 6 indicators instead of just the VIX?
Answer: No single indicator captures the full picture. VIX measures implied volatility expectations but can spike on isolated events (earnings, geopolitics) without a genuine regime change. By combining VIX with breadth (participation), the yield curve (recession signal), SPX momentum (trend), credit spreads (financial stress), and intermarket signals (flight-to-safety), you get a multi-dimensional view that's far more robust. A regime change confirmed by 5/6 indicators is high-confidence; a VIX spike alone might be a false alarm. The voting system prevents any single indicator from triggering unnecessary strategy shifts.
Quiz 3: Your regime score hovers between 0.39 and 0.41 for a week. What's the problem and how do you fix it?
Answer: This is regime whiplash — the score oscillates around a boundary, causing the scanner to switch strategies daily. This destroys performance because each switch incurs transaction costs and the conflicting strategies cancel each other out. The fix is hysteresis: require the score to move at least 0.05 past a boundary before switching. Alternatively, use a 3-day exponential moving average of the regime score instead of the raw daily value. This smoothing prevents daily flip-flopping while still responding to genuine regime changes within 3-5 days.
Quiz 4: In a Recovery regime, should you immediately go full Risk-On?
Answer: No. Recovery is a transition from Risk-Off toward Risk-On, but it's not confirmed Risk-On yet. Bear market rallies look exactly like Recovery in the early stages. The correct approach is gradual re-engagement: increase exposure from 30-50% (Risk-Off) to 50-70%, favor a mix of momentum and mean-reversion strategies, and keep some hedges in place. Only shift to full Risk-On (80-100% exposure, pure momentum) when the regime score sustainably crosses the Risk-On threshold for 3-5 consecutive days. The asymmetry principle applies: be slow to return to offense.
Quiz 5: Why is BTC included as a regime indicator (even at only 5% weight)?
Answer: Bitcoin serves as a liquidity canary. BTC trades 24/7 and is highly sensitive to global liquidity conditions. When central banks tighten (reducing liquidity), BTC often sells off before traditional markets react — it's a leading indicator because it trades outside market hours and attracts speculative capital that evaporates first in risk-off environments. At 5% weight, it doesn't drive regime decisions on its own, but it provides an early confirmation signal. When BTC drops 15% while other indicators are still Risk-On, it's a warning that liquidity conditions are deteriorating underneath the surface.
Quiz 6: Your scanner ran a momentum strategy in Risk-Off and lost 8R in a month. What went wrong?
Answer: Either the regime classifier failed (didn't detect Risk-Off in time) or the strategy rotation didn't execute (scanner kept running momentum despite the regime label). Diagnostic steps: (1) Check the regime classifier — were the indicators actually showing Risk-Off during that period? If yes, the classifier worked but the strategy rotation logic had a bug. (2) If the classifier didn't detect Risk-Off, check which indicators lagged. (3) Review the transition rules — perhaps the 3-day confirmation delay was too slow for a fast selloff. The 8R loss in a month is unacceptable and should trigger a kill-switch review before resuming live trading.
Key Takeaways

Key Takeaways — Part 4

Part 5 of 6
Backtesting Your Scanner — Proving It Works
Build a Stock Scanner That Actually Works4/6