Skip to main content

regime detection filtering framework - how i adapt to market conditions

august forcing me to rely on filters.

figured worth explaining how regime detection works. learned a lot from options selling regime discussions on NexusFi about adapting to conditions.

the problem
#

strategies don’t work in all conditions.

mean reversion thrives in range-bound markets.

momentum needs trending markets.

trading everything = losses.

solution:

detect market regime.

filter trades based on regime confidence.

regime detection framework
#

inputs:

VIX (volatility regime)

correlation (market cohesion)

volume (participation)

output:

regime confidence score 0-1.

trade acceptance threshold.

the code
#

import numpy as np
import pandas as pd
from typing import Tuple, Dict
from dataclasses import dataclass
from datetime import datetime, timedelta

@dataclass
class RegimeState:
    """Market regime state representation"""
    vix: float
    correlation: float
    volume_pct: float
    confidence: float
    regime_type: str
    timestamp: datetime

class RegimeDetector:
    """
    Detects market regimes and calculates trade acceptance thresholds.

    Strategy thrives in:
    - VIX 14-19 (optimal volatility)
    - Correlation 0.4-0.7 (diversification exists)
    - Volume >80% of 20-day avg (liquidity present)
    """

    def __init__(
        self,
        vix_optimal_range: Tuple[float, float] = (14.0, 19.0),
        vix_acceptable_range: Tuple[float, float] = (12.0, 25.0),
        corr_optimal_range: Tuple[float, float] = (0.4, 0.7),
        corr_acceptable_range: Tuple[float, float] = (0.3, 0.8),
        volume_threshold: float = 0.80,
        lookback_days: int = 20
    ):
        self.vix_optimal = vix_optimal_range
        self.vix_acceptable = vix_acceptable_range
        self.corr_optimal = corr_optimal_range
        self.corr_acceptable = corr_acceptable_range
        self.volume_threshold = volume_threshold
        self.lookback_days = lookback_days

        # Historical regime states
        self.regime_history: list[RegimeState] = []

    def calculate_vix_score(self, vix: float) -> float:
        """
        Calculate VIX component score.

        Returns:
            1.0 if in optimal range
            0.5-1.0 if in acceptable range
            0.0-0.5 if outside acceptable range
        """
        if self.vix_optimal[0] <= vix <= self.vix_optimal[1]:
            return 1.0

        if self.vix_acceptable[0] <= vix <= self.vix_acceptable[1]:
            # Linear interpolation in acceptable range
            if vix < self.vix_optimal[0]:
                dist = self.vix_optimal[0] - vix
                max_dist = self.vix_optimal[0] - self.vix_acceptable[0]
            else:
                dist = vix - self.vix_optimal[1]
                max_dist = self.vix_acceptable[1] - self.vix_optimal[1]

            return 0.5 + (0.5 * (1 - dist / max_dist))

        # Outside acceptable range - severe penalty
        if vix < self.vix_acceptable[0]:
            # Too low VIX
            dist = self.vix_acceptable[0] - vix
            return max(0.0, 0.5 - (dist / 10))
        else:
            # Too high VIX
            dist = vix - self.vix_acceptable[1]
            return max(0.0, 0.5 - (dist / 10))

    def calculate_correlation_score(self, correlation: float) -> float:
        """
        Calculate correlation component score.

        Returns:
            1.0 if in optimal range
            0.5-1.0 if in acceptable range
            0.0-0.5 if outside acceptable range
        """
        if self.corr_optimal[0] <= correlation <= self.corr_optimal[1]:
            return 1.0

        if self.corr_acceptable[0] <= correlation <= self.corr_acceptable[1]:
            if correlation < self.corr_optimal[0]:
                dist = self.corr_optimal[0] - correlation
                max_dist = self.corr_optimal[0] - self.corr_acceptable[0]
            else:
                dist = correlation - self.corr_optimal[1]
                max_dist = self.corr_acceptable[1] - self.corr_optimal[1]

            return 0.5 + (0.5 * (1 - dist / max_dist))

        # Outside acceptable range
        if correlation < self.corr_acceptable[0]:
            # Too low correlation (no diversification)
            return max(0.0, 0.5 - (self.corr_acceptable[0] - correlation))
        else:
            # Too high correlation (no edge)
            return max(0.0, 0.5 - (correlation - self.corr_acceptable[1]))

    def calculate_volume_score(
        self,
        current_volume: float,
        historical_volumes: np.ndarray
    ) -> float:
        """
        Calculate volume component score.

        Args:
            current_volume: Today's volume
            historical_volumes: Last N days volumes

        Returns:
            0.0-1.0 score based on volume relative to average
        """
        avg_volume = np.mean(historical_volumes)
        volume_ratio = current_volume / avg_volume

        if volume_ratio >= 1.0:
            # Above average volume = good
            return 1.0
        elif volume_ratio >= self.volume_threshold:
            # Acceptable volume range
            dist = 1.0 - volume_ratio
            max_dist = 1.0 - self.volume_threshold
            return 0.5 + (0.5 * (1 - dist / max_dist))
        else:
            # Below threshold - severe penalty
            return max(0.0, volume_ratio / self.volume_threshold * 0.5)

    def detect_regime(
        self,
        vix: float,
        correlation: float,
        volume: float,
        historical_volumes: np.ndarray,
        timestamp: datetime = None
    ) -> RegimeState:
        """
        Detect current market regime and calculate confidence.

        Args:
            vix: Current VIX level
            correlation: Current market correlation
            volume: Current trading volume
            historical_volumes: Historical volume data
            timestamp: Current timestamp

        Returns:
            RegimeState with confidence and regime type
        """
        # Calculate component scores
        vix_score = self.calculate_vix_score(vix)
        corr_score = self.calculate_correlation_score(correlation)
        vol_score = self.calculate_volume_score(volume, historical_volumes)

        # Weighted average (VIX and volume matter more)
        confidence = (
            0.4 * vix_score +
            0.3 * vol_score +
            0.3 * corr_score
        )

        # Determine regime type
        if confidence >= 0.75:
            regime_type = "optimal"
        elif confidence >= 0.60:
            regime_type = "acceptable"
        elif confidence >= 0.40:
            regime_type = "challenging"
        else:
            regime_type = "hostile"

        # Create regime state
        state = RegimeState(
            vix=vix,
            correlation=correlation,
            volume_pct=volume / np.mean(historical_volumes),
            confidence=confidence,
            regime_type=regime_type,
            timestamp=timestamp or datetime.now()
        )

        # Store in history
        self.regime_history.append(state)

        return state

    def calculate_acceptance_threshold(self, regime_confidence: float) -> float:
        """
        Calculate trade acceptance threshold based on regime confidence.

        Higher confidence = lower threshold (accept more trades)
        Lower confidence = higher threshold (accept fewer trades)

        Args:
            regime_confidence: 0-1 regime confidence score

        Returns:
            Acceptance threshold for trade quality score
        """
        # Inverse relationship: low confidence = high threshold
        # Optimal regime (0.8+ confidence) = 0.5 threshold (accept 50%+)
        # Hostile regime (0.3 confidence) = 0.8 threshold (accept 20%)

        if regime_confidence >= 0.75:
            # Optimal conditions - accept more trades
            return 0.50
        elif regime_confidence >= 0.60:
            # Acceptable conditions - normal acceptance
            return 0.60
        elif regime_confidence >= 0.40:
            # Challenging conditions - selective
            return 0.70
        else:
            # Hostile conditions - extremely selective
            return 0.80

    def should_accept_trade(
        self,
        trade_quality_score: float,
        regime_state: RegimeState
    ) -> Tuple[bool, str]:
        """
        Determine if trade should be accepted based on regime and quality.

        Args:
            trade_quality_score: 0-1 quality score for this trade
            regime_state: Current regime state

        Returns:
            (accept: bool, reason: str)
        """
        threshold = self.calculate_acceptance_threshold(regime_state.confidence)

        if trade_quality_score >= threshold:
            return (
                True,
                f"ACCEPT: quality {trade_quality_score:.2f} >= threshold {threshold:.2f} "
                f"(regime: {regime_state.regime_type}, confidence: {regime_state.confidence:.2f})"
            )
        else:
            return (
                False,
                f"REJECT: quality {trade_quality_score:.2f} < threshold {threshold:.2f} "
                f"(regime: {regime_state.regime_type}, confidence: {regime_state.confidence:.2f})"
            )

    def get_regime_summary(self, lookback_days: int = 7) -> Dict:
        """
        Get summary statistics for recent regime history.

        Args:
            lookback_days: Number of days to summarize

        Returns:
            Dictionary with regime statistics
        """
        if not self.regime_history:
            return {}

        cutoff = datetime.now() - timedelta(days=lookback_days)
        recent = [s for s in self.regime_history if s.timestamp >= cutoff]

        if not recent:
            return {}

        return {
            'avg_confidence': np.mean([s.confidence for s in recent]),
            'avg_vix': np.mean([s.vix for s in recent]),
            'avg_correlation': np.mean([s.correlation for s in recent]),
            'avg_volume_pct': np.mean([s.volume_pct for s in recent]),
            'regime_distribution': {
                regime: sum(1 for s in recent if s.regime_type == regime) / len(recent)
                for regime in ['optimal', 'acceptable', 'challenging', 'hostile']
            },
            'days_analyzed': len(recent)
        }


# Example usage
if __name__ == "__main__":
    detector = RegimeDetector()

    # August 2024 conditions
    august_vix = 16.0
    august_corr = 0.54
    august_volume = 850000  # 30% below normal
    historical_vol = np.array([1200000] * 20)  # Normal volume

    # Detect regime
    state = detector.detect_regime(
        vix=august_vix,
        correlation=august_corr,
        volume=august_volume,
        historical_volumes=historical_vol
    )

    print(f"Regime Type: {state.regime_type}")
    print(f"Confidence: {state.confidence:.2f}")
    print(f"VIX: {state.vix}")
    print(f"Correlation: {state.correlation}")
    print(f"Volume %: {state.volume_pct:.1%}")

    # Calculate acceptance threshold
    threshold = detector.calculate_acceptance_threshold(state.confidence)
    print(f"\nAcceptance Threshold: {threshold:.2f}")

    # Test trade acceptance
    trade_score = 0.65
    accept, reason = detector.should_accept_trade(trade_score, state)
    print(f"\nTrade Decision: {reason}")

how it works in august
#

current conditions:

VIX: 16.0 (optimal)

correlation: 0.54 (acceptable)

volume: -29% vs avg (terrible)

component scores:

VIX score: 1.0 (perfect)

correlation score: 0.7 (acceptable)

volume score: 0.36 (terrible)

regime confidence:

(0.4 × 1.0) + (0.3 × 0.36) + (0.3 × 0.7) = 0.718

regime type: acceptable (borderline challenging)

acceptance threshold: 0.60

result:

only trades scoring 0.60+ quality accepted.

vs normal 0.50 threshold.

39% acceptance rate.

why this matters
#

without regime detection:

august: trade all signals.

volume sucks → slippage kills edge.

result: -5% month easily.

with regime detection:

august: filter 61% of signals.

only best setups.

result: +0.21% month (survival).

difference:

-5% vs +0.21% = 5.21% preserved.

on $434k account = $22,600 saved.

filters work.

adjustments over time
#

april 2024:

optimal regime (confidence 0.82).

acceptance threshold 0.50.

53 trades, 74% win rate.

august 2024:

challenging regime (confidence 0.69).

acceptance threshold 0.60-0.70.

14 trades, 71% win rate.

fewer trades but similar win rate.

quality over quantity.

tonight (august 14, 5:30am)
#

regime detection framework explained.

august forcing reliance on filters.

component scores:

VIX: 1.0 (optimal)

volume: 0.36 (terrible)

correlation: 0.7 (acceptable)

regime confidence: 0.69-0.72 (challenging).

acceptance threshold: 0.60-0.70.

rejection rate: 60%+.

preserving capital vs forcing trades.

survival mode working.


5:30am wednesday. regime detection framework. august conditions: VIX 16.0 (optimal), correlation 0.54 (acceptable), volume -29% (terrible). regime confidence 0.69 (challenging). acceptance threshold 0.60 = 39% acceptance rate. filters preserving capital. without filtering: -5% month easily. with filtering: +0.21% survival. $22k+ saved.

-AK

Related

filtering aggressively in high vol - survival mode not growth mode
week 2 may. VIX still elevated. aggressive filtering required. current market conditions # VIX range: 19-26 this week
regime detection improvements - faster market adaptation working
week 3 april going strong. adaptive strategy crushing it. been refining regime detection logic. current performance (apr 1-17) # trades: 29
strategy overhaul - adapting algos to new market regime
february crushed my strategies. mean reversion dropped from 81% to 57% win rate. market regime changed. strategies need to adapt. been discussing regime adaptation on r/algotrading. other algo traders dealing with same shit.
backtesting overfitting - how i avoid curve-fitting my algos
backtesting is where most algo traders hurt themselves. they optimize parameters until strategy looks perfect on historical data. then go live and it fails immediately. classic overfitting. learned this the hard way. saw countless traders on NexusFi backtesting discussions make same mistake when i joined in 2023.
momentum breakout strategy - how it works
momentum strategy has 5 wins, 0 losses. time to explain how it works. core concept # capture trending moves after consolidation breaks.
building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.