← Back to list

defensive_vol_calm VALIDATED PASS

Defensive Strategy: Ultra-Low Activity Filter ============================================== Role: DEFENSIVE - "Capital preservation first" Concept - TRULY DEFENSIVE: This strategy is designed to be a "do nothing most of the time" filter. It only trades under extremely rare conditions where: 1. Volatility is at multi-day lows (ATR z-score < -2) 2. Very strong volume surge (3x average) 3. Clear directional bias from prior bars 4. Time-based exit to limit exposure The goal is NOT to make money

Symbol: BTC | Exchange: Bitfinex | Role: defensive

3/6
Profitable Years
+2.1%
Total Return
53.7%
Avg Win Rate
0.53
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe
2025-07 -0.2% 50.0% 2 0.3% -0.47
2025-08 +0.4% 75.0% 4 0.0% 2.04
2025-09 +0.1% 33.3% 3 0.1% 0.58
2025-10 -0.5% 50.0% 4 0.8% -1.07
2025-11 +2.3% 60.0% 10 0.4% 1.59

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+2.1% 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 +2.1% OK 2026-01→ongoing +0.0% PASS

AI Review Score: 35/100

overfitting concentration cost_sensitive inconsistent
## Critical Issues ### 1. Severe Overfitting to Specific Market Regime The strategy shows classic signs of overfitting to a specific market period: - **Extreme concentration**: 10 out of 23 total trades (43%) occur in a single month - **Wildly inconsistent monthly performance**: Ranges from -0.5% to +2.3%, with most months near-flat - **Most return from one period**: The 2.14% total return is almost entirely driven by one month's 2.3% return This is a **RED FLAG** for 1-minute strategies. A truly robust microstructure edge should work consistently across different market conditions, not suddenly "wake up" in one specific month. ### 2. Overly Specific Parameter Combinations The strategy stacks multiple highly-tuned conditions: - ATR z-score threshold of **exactly -2.0** (why not -1.5 or -2.5?) - Volume surge of **exactly 3.0x** (why not 2.5x or 3.5x?) - **Exactly 3** consecutive bars (why not 2 or 4?) - 360-bar cooldown (why exactly 6 hours?) - 15-bar hold period (why exactly 15 minutes?) These precise values suggest curve-fitting to historical noise rather than capturing a genuine microstructure phenomenon. Round numbers like 20, 120, 360 are good, but the **combination** of multiple specific thresholds is suspicious. ### 3. Cost Sensitivity at Low Trade Frequency With only 23 trades over 5 months (average 0.15 trades/day): - **0.10% slippage cost** = 23 trades × 0.10% = **2.3% total cost** - **Return after costs**: 2.14% - 2.3% = **-0.16%** (nearly breakeven, but negative) - Average trade profit: 0.093%, which is barely 1x the round-trip cost The "defensive" label doesn't excuse being cost-sensitive. A defensive strategy should either: - Trade even LESS with higher conviction - Have wider edges per trade to survive costs This strategy is stuck in the middle: too few trades to diversify risk, too thin edges to survive execution reality. ### 4. Statistical Reliability: Not Enough Data 23 trades is **insufficient sample size** to validate any strategy: - With 60% win rate and 23 trades, even a coin-flip strategy could produce similar results - The one high-performing month (10 trades, 60% WR) could easily be luck - Need 100+ trades minimum to distinguish skill from noise at 1-minute frequency ### 5. The "Extreme Calm Before Volume Surge" Thesis is Questionable The core hypothesis: "ATR < -2 std + 3x volume surge = edge" is theoretically weak: - **Why would extreme calm + sudden volume be predictive?** - If it's a breakout, the move already happened (you're late) - If it's a false breakout, you're catching a knife - The 3-consecutive-bar filter tries to add directionality, but this is **reactive**, not predictive - 15-minute hold is arbitrary—no microstructure edge has a fixed time decay ### 6. Month-Over-Month Validation Failure Breaking down the monthly Sharpe ratios: - Two negative months (-0.47, -1.07) - One excellent month (+2.04) - Two mediocre months (+0.58, +1.59) This is **NOT consistent**. A real microstructure edge (VWAP reversion, RSI extremes, vol spike mean reversion) should show positive Sharpe in 4 out of 5 months, not this wild variance. ## What Would Make This Strategy Better? 1. **Simplify conditions**: Remove at least 2 of the 5 entry filters. Too many ANDs = curve-fitting 2. **Widen the edge**: If you're only trading 0.15x/day, each trade should target 0.3%+ profit (3x cost buffer) 3. **Dynamic exits**: Use price-based exits (e.g., VWAP reversion complete) instead of arbitrary 15-bar timer 4. **Prove consistency**: Should work in 4+ out of 5 months with Sharpe > 0.3 in each 5. **Reduce parameters**: Try just ONE condition: "ATR z-score < -2 AND volume > 3x" with 3-bar direction. See if that alone works. If not, the edge doesn't exist. ## Verdict This is a **curve-fit defensive strategy** that accidentally caught one good month. The "defensive" framing is misleading—being flat most of the time isn't defensive if your rare trades are cost-sensitive and inconsistent. **Score: 35/100** - The strategy has some structure (reasonable warmup, no obvious lookahead), but the concentration risk, cost sensitivity, and month-to-month inconsistency make it unviable for live 1-minute trading.
Reviewed: 2026-01-14T12:41:03.408251

Source Code

#!/usr/bin/env python3
"""
Defensive Strategy: Ultra-Low Activity Filter
==============================================

Role: DEFENSIVE - "Capital preservation first"

Concept - TRULY DEFENSIVE:
This strategy is designed to be a "do nothing most of the time" filter.
It only trades under extremely rare conditions where:
1. Volatility is at multi-day lows (ATR z-score < -2)
2. Very strong volume surge (3x average)
3. Clear directional bias from prior bars
4. Time-based exit to limit exposure

The goal is NOT to make money - it's to NOT LOSE money.
A flat equity curve is success for this strategy.

Design philosophy:
- Trade count target: ~20-30 over 5 months (very few)
- Focus on avoiding bad trades
- Preserving capital is the primary objective

Defensive requirements: sharpe >= 0, DD < 10%, must not lose
"""

import sys
sys.path.insert(0, '/root/trade_1m')
from lib import load_data, ema, sma, rsi, atr
from strategy import backtest_strategy, run_strategy


def init_strategy():
    return {
        'name': 'defensive_vol_calm',
        'role': 'defensive',  # CRITICAL: defensive role
        'warmup': 300,  # 5 hours warmup
        'subscriptions': [
            {'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '1m'},
        ],
        'parameters': {
            'atr_period': 20,             # ATR period
            'atr_lookback': 120,          # 2 hours lookback for stats
            'extreme_calm_threshold': -2.0,  # EXTREME calm only (2 std below mean)
            'volume_surge': 3.0,          # Volume must be 3x average (stricter)
            'min_bars_between': 360,      # 6 hours between trades
            'hold_bars': 15,              # 15 min hold
            'stop_loss_pct': 0.4,         # Tight stop
            'consecutive_bars': 3,        # Need 3 bars in same direction
        }
    }


def process_time_step(ctx):
    """
    Ultra-defensive trading: mostly flat, very rare entries.

    Entry requires ALL of:
    1. ATR z-score < -2.0 (extreme calm - 2 std below mean)
    2. Volume surge 3x average
    3. 3 consecutive bars in same direction
    4. 6+ hours since last trade

    Exit:
    - Time-based (15 bars)
    - OR stop loss hit (0.4%)
    """
    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']
    extreme_calm_threshold = params['extreme_calm_threshold']
    volume_surge = params['volume_surge']
    min_bars_between = params['min_bars_between']
    hold_bars = params['hold_bars']
    consecutive_bars = params['consecutive_bars']

    if i < atr_lookback + atr_period + consecutive_bars:
        return []

    # Lazy-compute indicators
    if 'atr_values' not in state:
        highs = [b.high for b in bars]
        lows = [b.low for b in bars]
        closes = [b.close for b in bars]
        volumes = [b.volume for b in bars]
        state['atr_values'] = atr(highs, lows, closes, atr_period)
        state['volumes'] = volumes
        state['closes'] = closes
        state['last_trade_bar'] = -min_bars_between

    atr_values = state['atr_values']
    volumes = state['volumes']
    closes = state['closes']

    current_atr = atr_values[i]
    if current_atr is None:
        return []

    # ATR z-score with longer lookback
    recent_atrs = [atr_values[j] for j in range(max(0, i - atr_lookback), i) if 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

    # Volume ratio
    recent_volumes = volumes[max(0, i-60):i]
    avg_volume = sum(recent_volumes) / len(recent_volumes) if recent_volumes else 1
    current_volume = volumes[i]
    vol_ratio = current_volume / avg_volume if avg_volume > 0 else 0

    actions = []

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

        # Time-based exit only
        if bars_held >= hold_bars:
            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',
            })
    else:
        # Check cooldown (6 hours)
        bars_since_last = i - state.get('last_trade_bar', -min_bars_between)
        if bars_since_last < min_bars_between:
            return []

        # ULTRA-STRICT entry conditions
        extreme_calm = atr_zscore <= extreme_calm_threshold
        high_volume = vol_ratio >= volume_surge

        if extreme_calm and high_volume:
            # Check for consecutive bars in same direction
            bullish_count = 0
            bearish_count = 0

            for j in range(i - consecutive_bars + 1, i + 1):
                if closes[j] > closes[j-1]:
                    bullish_count += 1
                elif closes[j] < closes[j-1]:
                    bearish_count += 1

            # Need ALL consecutive bars in same direction
            if bullish_count == consecutive_bars:
                state['last_trade_bar'] = i
                actions.append({
                    'action': 'open_long',
                    'symbol': 'tBTCUSD',
                    'exchange': 'bitfinex',
                    'size': 1.0,
                    'stop_loss_pct': params['stop_loss_pct'],
                })
            elif bearish_count == consecutive_bars:
                state['last_trade_bar'] = i
                actions.append({
                    'action': 'open_short',
                    'symbol': 'tBTCUSD',
                    'exchange': 'bitfinex',
                    'size': 1.0,
                    'stop_loss_pct': params['stop_loss_pct'],
                })

    return actions


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

    print("\n" + "="*60)
    print("Testing: defensive_vol_calm (Ultra-Low Activity)")
    print("Role: defensive")
    print("="*60)

    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 = []
    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)

        metrics = calc_metrics(std_trades)
        month = start[5:7]
        ret = metrics.get('return', 0)
        tr = metrics.get('trades', 0)
        wr = metrics.get('win_rate', 0)
        print(f"  2025-{month}: {ret:+.2f}% | {tr} trades | WR: {wr:.0f}%")

    overall = calc_metrics(all_trades)
    print(f"\n  {'='*50}")
    print(f"  TOTAL: {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}%")

    # Stress tests
    print(f"\n  STRESS TESTS:")
    trades_count = overall.get('trades', 0)
    SLIPPAGE = 0.05  # 5bp per side
    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 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}%")

    print(f"\n  Defensive validation:")
    print(f"  - Sharpe >= 0: {'PASS' if overall.get('sharpe', 0) >= 0 else 'FAIL'}")
    print(f"  - DD < 10%: {'PASS' if overall.get('max_dd', 0) < 10 else 'FAIL'}")
    print(f"  - Minimal loss after costs: {'PASS' if after_slippage > -1 else 'FAIL'}")
    print(f"    (Defensive goal: near-zero, small loss acceptable)")