← Back to list

vol_control_btc_regime_switch VALIDATED FAIL

Auto-discovered strategy

Symbol: BTC | Exchange: Bitfinex | Role: vol_control

4/6
Profitable Years
+3.5%
Total Return
49.6%
Avg Win Rate
0.33
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe
2020 +1.0% 49.0% 49 1.4% 0.52
2021 -0.2% 45.1% 51 2.1% -0.08
2022 +0.6% 44.0% 50 2.0% 0.32
2023 -0.6% 43.9% 41 1.4% -0.38
2024 +1.5% 58.7% 46 1.1% 0.90
2025 +1.1% 57.1% 42 1.3% 0.72
2025-07 +0.3% 66.7% 3 0.2% 0.56
2025-08 +0.5% 75.0% 4 0.3% 0.96
2025-09 +0.7% 66.7% 3 0.2% 1.37
2025-10 +0.2% 50.0% 4 0.2% 0.66
2025-11 +0.1% 50.0% 4 0.5% 0.09

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
+0.2% OOS Return
0.00 Median Sharpe
0.000 Score
Window Train Period Val Period Val Return Val Test Period Test Return Status
WF-1 2025-07→2025-11 2025-12→2025-12 +0.2% OK 2026-01→ongoing +0.1% FAIL

AI Review Score: 45/100

overfitting cost_sensitive concentration inconsistent
## Vol Control Regime Switch Strategy Review ### Critical Issues **1. Severe Validation Failure (BLOCKING)** The strategy produced only 4 trades in validation, falling 60% short of the minimum 10-trade requirement for vol_control role. This is a fundamental failure - the regime it was trained to detect may not recur reliably. **2. Parameter Overfitting** The strategy uses highly specific thresholds that smell of curve-fitting: - ATR z-score compression at exactly -1.0 → -0.3 transition - 0.25% minimum move (not 0.2% or 0.3%) - 0.45% take profit (why not 0.4% or 0.5%?) - 15-bar lookback for regime detection These non-round parameters suggest optimization to historical data rather than robust microstructure logic. **3. Extremely Sparse Trading + Cost Sensitivity** With ~3 trades/month frequency: - Each trade must carry 0.1% round-trip cost (0.05% slippage per side) - Strategy return of 1.77% across 18 trades = 0.10% avg per trade - After costs: near breakeven or negative - **The edge is too thin for the friction at this frequency** **4. Multi-Year Inconsistency** The strategy shows wild variance across longer periods: - Returns range from -0.6% to +1.5% annually - Training period (last 5 months) shows 1.77% return - But 2021 was negative, 2023 was negative - Small sample + high variance = unstable edge **5. Concentration Risk** With only 18 total trades: - Removing best 2-3 winners likely flips strategy negative - Not enough statistical power to distinguish skill from luck - Monte Carlo resampling would show massive uncertainty bands ### Microstructure Logic Assessment The core concept "trade the exit from vol compression" has theoretical merit: - Vol regimes do exist and transition - Early momentum after compression can be profitable However, the implementation fails because: - Detection is too specific (exact z-score thresholds) - 7-day cooldown creates artificial scarcity - No evidence this regime occurs frequently enough - 15-bar backward check may capture noise, not regime ### Execution Concerns At 1-minute resolution: - ATR calculations are stable (good) - Entry logic checks historical bars, not same-bar (good) - BUT: Vol spike exit happens on current bar's z-score, which uses current ATR - This could be lookahead if exiting intra-bar based on bar's final ATR - Not critical since it's exit-only, but worth noting ### What Would Need to Change For this to be viable: 1. **Higher frequency**: 3 trades/month cannot amortize costs. Need 20+ trades/month minimum. 2. **Simpler parameters**: Use round thresholds (z-score = -1 → 0, not -1 → -0.3) 3. **Larger edge per trade**: 0.10% avg is too thin. Need 0.2%+ after costs. 4. **Validation consistency**: Must produce 10+ trades in any test period. ### Cost Reality Check - 18 trades × 0.1% cost = 1.8% total drag - Strategy return: 1.77% - **Net after costs: essentially zero** This is not a viable 1-minute strategy. ### Final Verdict **Score: 45/100 (Poor)** The strategy fails the fundamental test for 1-minute trading: **edge must be large enough and frequent enough to overcome friction**. With only 3 trades/month averaging 0.10% profit, costs consume the entire edge. The validation failure (4 trades < 10 minimum) confirms the regime doesn't recur reliably. This is a case where a potentially sound concept (vol regime transition) was over-optimized to historical data and doesn't generalize. The specific z-score thresholds and 7-day cooldown created artificial performance in training that collapsed in validation.
Reviewed: 2026-01-14T14:26:43.863794

Source Code

"""
Vol Control Strategy: Regime Switch Detection for tBTCUSD
==========================================================

Role: VOL_CONTROL - "Trade the transition out of low volatility"

Exchange: bitfinex
Symbol: tBTCUSD
Timeframe: 1m

Concept:
- Monitor ATR z-score for regime changes
- When vol EXITS compression (z crossing from < -1 to > -0.3), follow the move
- This captures the initial impulse of vol expansion
- Very selective: ~3 trades per month with 7-day cooldown

WHY THIS WORKS on 1-minute:
- Vol compression ALWAYS ends (mean reversion of vol itself)
- The exit from compression is often directional
- By following the early part of the move, we capture momentum
- Exit before vol gets too high where execution suffers
- Long cooldown ensures only highest quality setups

Key insight:
- Don't trade IN low vol (moves too small)
- Trade the TRANSITION out of low vol (directional impulse)

Entry (all must be true):
1. ATR z-score WAS < -1 (15 bars ago = compression)
2. ATR z-score NOW > -0.3 (vol rising = compression exit)
3. Price moved > 0.25% in last 15 bars (directional impulse)
4. 7-day cooldown between trades

Direction:
- Long if price rose during compression exit
- Short if price fell during compression exit

Exit:
- Take profit at 0.45%
- OR max hold time (25 bars)
- OR vol spikes (ATR z > 2)
- OR stop loss hit (0.30%)

Training Performance (2025-07 to 2025-11):
- Total Return: +1.77%
- Sharpe: 1.53
- Max DD: 0.5%
- Trades: 18
- Win Rate: 61%
- Avg Trade: 0.10%
- 5/5 months profitable

Vol Control requirements: sharpe >= 0.2, DD < 12%, trades >= 10, loss < -3%
"""

import sys
sys.path.insert(0, '/root/trade_1m')
from lib import atr


def init_strategy():
    return {
        'name': 'vol_control_btc_regime_switch',
        'role': 'vol_control',  # CRITICAL: vol_control role
        'warmup': 200,  # ~3+ hours warmup for ATR stats
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '1m'},
        ],
        'parameters': {
            # ATR parameters for vol regime detection
            'atr_period': 30,           # 30 min ATR
            'atr_lookback': 180,        # 3 hour lookback for z-score
            # Regime switch detection
            'vol_compression': -1.0,    # Compression threshold
            'vol_expansion': -0.3,      # Exit-compression threshold
            'vol_spike_disable': 2.0,   # NO trades above this
            'lookback_bars': 15,        # 15 bars to check for compression exit
            # Price move filter
            'min_move_pct': 0.25,       # Larger directional move = higher quality entries
            # Trade management
            'max_hold_bars': 25,        # 25 min max hold (let winners run longer)
            'take_profit_pct': 0.45,    # 0.45% take profit
            'stop_loss_pct': 0.30,      # 0.30% stop
            'cooldown_bars': 10080,     # 7 day cooldown (~3 trades per month)
        }
    }


def process_time_step(ctx):
    """
    Vol control strategy: Trade regime switch from compression to expansion.

    CRITICAL: No trades when ATR z-score > 2 (hard disable)
    """
    key = ('tBTCUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']
    state = ctx['state']

    atr_period = params['atr_period']
    atr_lookback = params['atr_lookback']
    vol_comp = params['vol_compression']
    vol_exp = params['vol_expansion']
    vol_spike = params['vol_spike_disable']
    lookback = params['lookback_bars']
    min_move = params['min_move_pct']
    max_hold = params['max_hold_bars']
    cooldown = params['cooldown_bars']

    # Initialize state
    if 'last_trade_bar' not in state:
        state['last_trade_bar'] = -cooldown
        state['atr_values'] = None
        state['atr_zscores'] = []

    # Check minimum data
    required = max(atr_lookback + atr_period, lookback + 10)
    if i < required:
        return []

    # Lazy-compute ATR (once per backtest run)
    if state['atr_values'] is None:
        highs = [b.high for b in bars]
        lows = [b.low for b in bars]
        closes = [b.close for b in bars]
        state['atr_values'] = atr(highs, lows, closes, atr_period)

    atr_values = state['atr_values']

    current_atr = atr_values[i] if i < len(atr_values) and atr_values[i] is not None else None
    if current_atr is None:
        return []

    # Compute ATR z-score
    recent_atrs = [atr_values[j] for j in range(max(0, i - atr_lookback), i)
                   if j < len(atr_values) and atr_values[j] is not None]

    if len(recent_atrs) < 30:
        return []

    atr_mean = sum(recent_atrs) / len(recent_atrs)
    atr_variance = sum((x - atr_mean) ** 2 for x in recent_atrs) / len(recent_atrs)
    atr_std = atr_variance ** 0.5 if atr_variance > 0 else 0.0001
    atr_zscore = (current_atr - atr_mean) / atr_std

    # Compute z-score from N bars ago
    past_atr = atr_values[i - lookback] if i >= lookback and atr_values[i - lookback] is not None else None
    if past_atr is None:
        return []

    past_recent = [atr_values[j] for j in range(max(0, i - lookback - atr_lookback), i - lookback)
                   if j < len(atr_values) and atr_values[j] is not None]
    if len(past_recent) < 30:
        return []

    past_mean = sum(past_recent) / len(past_recent)
    past_var = sum((x - past_mean) ** 2 for x in past_recent) / len(past_recent)
    past_std = past_var ** 0.5 if past_var > 0 else 0.0001
    past_zscore = (past_atr - past_mean) / past_std

    # Compute price move
    price_now = bars[i].close
    price_before = bars[i - lookback].close
    price_move_pct = (price_now - price_before) / price_before * 100

    actions = []

    # === VOL SPIKE EMERGENCY EXIT ===
    if atr_zscore > vol_spike:
        if key in positions:
            pos = positions[key]
            state['last_trade_bar'] = i
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            return [{
                'action': action_type,
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            }]
        return []

    # === EXIT LOGIC ===
    if key in positions:
        pos = positions[key]
        bars_held = i - pos.entry_bar

        should_exit = False

        # Time-based exit
        if bars_held >= max_hold:
            should_exit = True

        # Vol spikes too high - exit
        if atr_zscore > vol_spike:
            should_exit = True

        if should_exit:
            state['last_trade_bar'] = i
            action_type = 'close_long' if pos.side == 'long' else 'close_short'
            actions.append({
                'action': action_type,
                'symbol': 'tBTCUSD',
                'exchange': 'bitfinex',
            })
        return actions

    # === ENTRY LOGIC ===
    # Check cooldown
    if i - state['last_trade_bar'] < cooldown:
        return []

    # Check for regime switch: was compressed, now expanding
    was_compressed = past_zscore < vol_comp
    now_expanding = atr_zscore > vol_exp and atr_zscore < vol_spike

    if not (was_compressed and now_expanding):
        return []

    # Check for directional move during the transition
    if abs(price_move_pct) < min_move:
        return []

    # Long: price rose during compression exit
    if price_move_pct > min_move:
        state['last_trade_bar'] = i
        actions.append({
            'action': 'open_long',
            'symbol': 'tBTCUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
            'take_profit_pct': params['take_profit_pct'],
            'stop_loss_pct': params['stop_loss_pct'],
        })

    # Short: price fell during compression exit
    elif price_move_pct < -min_move:
        state['last_trade_bar'] = i
        actions.append({
            'action': 'open_short',
            'symbol': 'tBTCUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
            'take_profit_pct': params['take_profit_pct'],
            'stop_loss_pct': params['stop_loss_pct'],
        })

    return actions


if __name__ == '__main__':
    from strategy import backtest_strategy, run_strategy
    from lib import calc_metrics, Trade

    print("\n" + "="*60)
    print("Testing: vol_control_btc_regime_switch")
    print("Role: vol_control")
    print("Symbol: tBTCUSD @ bitfinex")
    print("="*60)

    # Run backtest on training data
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)

    # Get all trades for overall metrics
    test_periods = [
        ('2025-07-01', '2025-07-31'),
        ('2025-08-01', '2025-08-31'),
        ('2025-09-01', '2025-09-30'),
        ('2025-10-01', '2025-10-31'),
        ('2025-11-01', '2025-11-30'),
    ]

    all_trades = []
    monthly_returns = []
    for start, end in test_periods:
        trades, _, _ = run_strategy(init_strategy, process_time_step, start, end)
        std_trades = [
            Trade(
                entry_time=t.entry_time,
                entry_price=t.entry_price,
                exit_time=t.exit_time,
                exit_price=t.exit_price,
                side=t.side,
                pnl_pct=t.pnl_pct
            )
            for t in trades
        ]
        all_trades.extend(std_trades)
        m = calc_metrics(std_trades)
        monthly_returns.append(m.get('return', 0))

    overall = calc_metrics(all_trades)

    print(f"\n  {'='*50}")
    print(f"  OVERALL METRICS:")
    print(f"  Total Return: {overall.get('return', 0):+.2f}%")
    print(f"  Trades: {overall.get('trades', 0)}")
    print(f"  Sharpe: {overall.get('sharpe', 0):.2f}")
    print(f"  Max DD: {overall.get('max_dd', 0):.1f}%")
    print(f"  Win Rate: {overall.get('win_rate', 0):.0f}%")
    print(f"  Avg Trade: {overall.get('avg_trade', 0):.3f}%")

    # Monthly consistency check
    positive_months = sum(1 for r in monthly_returns if r > 0)
    total_return = overall.get('return', 0)
    if total_return > 0 and max(monthly_returns) > 0:
        max_month_contribution = max(monthly_returns) / total_return
    else:
        max_month_contribution = 1.0
    print(f"\n  MONTHLY CONSISTENCY:")
    print(f"  Monthly returns: {[f'{r:.1f}' for r in monthly_returns]}")
    print(f"  Positive months: {positive_months}/5")
    print(f"  Max month contribution: {max_month_contribution:.0%}")

    # Stress tests
    print(f"\n  STRESS TESTS:")
    trades_count = overall.get('trades', 0)
    SLIPPAGE = 0.05  # 5bp per side = 0.1% round trip
    total_cost = trades_count * 2 * SLIPPAGE
    after_slippage = overall.get('return', 0) - total_cost
    print(f"  After slippage ({SLIPPAGE*2:.2f}% round trip): {after_slippage:+.2f}%")

    delay_factor = 0.7  # Assume 30% edge loss from +1 bar delay
    after_delay = overall.get('return', 0) * delay_factor
    print(f"  After +1 bar delay (~30% edge loss): {after_delay:+.2f}%")

    worst_case = after_delay - total_cost
    print(f"  Worst case (both): {worst_case:+.2f}%")

    # Vol control validation requirements
    print(f"\n  VOL CONTROL VALIDATION:")
    sharpe_pass = overall.get('sharpe', 0) >= 0.2
    dd_pass = overall.get('max_dd', 0) < 12
    trades_pass = overall.get('trades', 0) >= 10
    loss_pass = overall.get('return', 0) > -3

    print(f"  - Sharpe >= 0.2: {'PASS' if sharpe_pass else 'FAIL'} ({overall.get('sharpe', 0):.2f})")
    print(f"  - DD < 12%: {'PASS' if dd_pass else 'FAIL'} ({overall.get('max_dd', 0):.1f}%)")
    print(f"  - Trades >= 10: {'PASS' if trades_pass else 'FAIL'} ({overall.get('trades', 0)})")
    print(f"  - Return > -3%: {'PASS' if loss_pass else 'FAIL'} ({overall.get('return', 0):+.2f}%)")

    all_pass = sharpe_pass and dd_pass and trades_pass and loss_pass
    print(f"\n  OVERALL: {'PASS' if all_pass else 'FAIL'}")