← Back to list

breakout_fade_eth_vwap_extreme VALIDATED FAIL

Auto-discovered strategy

Symbol: ETH | Exchange: Bitfinex | Role: breakout_fade

4/6
Profitable Years
+37.2%
Total Return
56.3%
Avg Win Rate
0.36
Avg Sharpe

Monthly Results

Click a period to view chart

Period Return Win Rate Trades Max DD Sharpe
2020 +22.8% 59.4% 1083 7.8% 2.01
2021 +19.8% 58.2% 1374 8.1% 1.47
2022 +10.9% 58.1% 1204 11.4% 0.96
2023 -11.5% 52.7% 628 13.3% -1.67
2024 -7.4% 53.5% 564 11.3% -0.93
2025 +2.6% 55.7% 673 11.3% 0.30

Performance Chart

Loading chart...

Walk-Forward Validation FAIL

0/1 Windows Profitable
-2.5% 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% FAIL 2026-01→ongoing -0.4% FAIL

AI Review Score: 35/100

cost_sensitive overfitting execution inconsistent
## Critical Issues ### 1. Cost Sensitivity - Fatal Flaw **Average trade: 0.04% vs 0.05% slippage threshold** This strategy is DOA at realistic costs. With ~334 trades generating 12.64% gross return, the avg trade is 0.038% - **below the minimum viable edge** for 1-minute trading. After 0.10% round-trip slippage (0.05% per side), this strategy would likely be net negative. The validation results confirm this: -2.15% on hold-out data. The author acknowledges this in comments but submitted anyway. This is not a tradeable strategy. ### 2. Overfitting to Market Regime **Performance cliff from 2022 onwards:** - 2020-2022: Sharpe 0.96-2.01, returns 10-23% - 2023-2024: Sharpe -1.67 to -0.93, returns -11.5% to -7.4% - Validation: Sharpe -0.83, return -2.15% This is classic overfitting to a specific market regime. The strategy was trained on 2025 data but shows it would have failed badly in 2023-2024. The dramatic performance degradation suggests the parameters are tuned to recent market microstructure that may not persist. ### 3. Execution Reality at 1-Minute Scale **Same-bar breakout detection is problematic:** The strategy detects false breakouts by checking if `bar.high > range_high and bar.close < range_high` within the same 1-minute bar. At 1-minute timeframes, this creates severe execution issues: - You can't know a bar will close back inside the range until the bar closes - By definition, you're entering at/near the close of the detection bar - This assumes you can execute at the close price, which is unrealistic with market orders - Real slippage will be worse during the volume spikes this strategy targets The 2.0x volume spike requirement means you're trying to enter during elevated volatility and increased spread costs. ### 4. Parameter Specificity **Suspiciously precise thresholds:** - VWAP threshold: 2.5 ATR (why not 2.0 or 3.0?) - Volume spike: 2.0x (round, but combined with other filters suggests optimization) - ATR z-score: 1.5 max (oddly specific) - Range period: 30 bars (acceptable) - Max hold: 30 bars (round) - Stop: 0.50%, Target: 0.35% (tight targets suggest curve-fitting to capture specific moves) The 0.35% profit target is concerning - it's barely 7x the avg trade size, leaving almost no room for execution slippage or adverse fills. ### 5. Inconsistent Monthly Performance **4/5 positive training months hides concentration:** While 4/5 months were positive in training, the validation failure suggests the training period happened to align with favorable microstructure conditions. The 3-hour cooldown (180 bars) limits trade frequency, meaning a few good setups per month can dominate returns. ### 6. Double-Filter Risk **VWAP extension + false breakout = too specific:** Requiring both: 1. Price 2.5+ ATR from VWAP, AND 2. False breakout with volume spike, AND 3. ATR z-score < 1.5 ...creates a very narrow opportunity set. The 334 trades over 5 months (67/month) is reasonable frequency, but the specificity of conditions suggests these were optimized to historical patterns rather than robust microstructure principles. ## What This Strategy Gets Right - **Core concept is sound**: Fading extended false breakouts is a legitimate microstructure edge - **VWAP mean reversion**: Using VWAP as an anchor is appropriate for 1-minute - **Volume confirmation**: Requiring volume spike adds conviction - **Time-based exit**: 30-bar max hold prevents dead capital - **Round parameters**: Most periods (30, 120, 60, 20, 180) are round numbers - **Cooldown logic**: 3-hour cooldown prevents overtrading ## Why It Fails 1. **Edge is too small**: 0.04% avg trade cannot survive real costs 2. **Optimized thresholds**: The 2.5 ATR, 0.35% target, and filter combinations appear curve-fit 3. **Execution assumptions**: Same-bar detection creates unrealistic fills 4. **Regime-dependent**: Works in trending/volatile periods, fails in others 5. **Validation failure**: -2.15% return on unseen data proves lack of robustness ## Verdict This strategy demonstrates a reasonable understanding of microstructure concepts but fails on execution reality and cost sensitivity. The author's own warning ("avg_trade ~0.04%, which is below the 0.05% cost floor") should have prevented submission. The validation metrics confirm this is not a robust edge: -0.83 Sharpe and -2.15% return show it doesn't generalize beyond the training period. **Not recommended for live trading.**
Reviewed: 2026-01-14T16:08:15.104366

Source Code

#!/usr/bin/env python3
"""
Breakout Fade Strategy: VWAP Extension + False Breakout
========================================================

Role: BREAKOUT_FADE - "Fade failed breakouts when price is extended from VWAP"

Concept:
- Identify micro range (30-bar high/low)
- Require price to be significantly extended from VWAP (2.5+ ATR)
- Detect false breakout (price breaks range but closes back inside)
- Fade the breakout by trading opposite direction
- Exit on VWAP reversion or time limit

WHY THIS WORKS on 1-minute:
- Most breakouts at 1m scale are noise, not real moves
- When price is already extended from VWAP, mean reversion is likely
- False breakouts + VWAP extension = double confirmation of reversal
- Volume spike confirms institutional interest in the rejection

Entry (Long fade - after false breakdown):
1. Price is 2.5+ ATR below 2h VWAP (oversold)
2. Price broke below 30-bar low (made new low)
3. Bar closed back above the low (rejection wick)
4. Volume spike (2x average)
5. ATR z-score < 1.5 (no crazy vol)

Entry (Short fade - after false breakup):
1. Price is 2.5+ ATR above 2h VWAP (overbought)
2. Price broke above 30-bar high (made new high)
3. Bar closed back below the high (rejection wick)
4. Volume spike (2x average)
5. ATR z-score < 1.5 (no crazy vol)

Exit:
- Price returns to VWAP
- Time-based (30 bars max)
- Take profit (0.35%)
- Stop loss (0.50%)
- Vol spike emergency (ATR z > 2)

Training Results (2025-07 to 2025-11):
- Total Return: +12.64%
- Trades: 334
- Sharpe: 2.23
- Max DD: 2.3%
- Win Rate: 60%
- Positive months: 4/5

IMPORTANT: This strategy has avg_trade ~0.04%, which is below the 0.05% cost floor.
After slippage costs, net return may be negative. Use at your own risk.

Breakout_fade requirements: sharpe >= 0.4, DD < 18%, trades >= 15, loss < -8%
"""

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


def init_strategy():
    return {
        'name': 'breakout_fade_eth_vwap_extreme',
        'role': 'breakout_fade',
        'warmup': 200,  # Warmup for 2h VWAP
        'subscriptions': [
            {'symbol': 'tETHUSD', 'exchange': 'bitfinex', 'timeframe': '1m'},
        ],
        'parameters': {
            # Range detection
            'range_period': 30,           # 30-bar range
            # VWAP extension
            'vwap_period': 120,           # 2h VWAP
            'vwap_threshold': 2.5,        # Must be 2.5+ ATR from VWAP
            # Volume confirmation
            'volume_spike': 2.0,          # 2x average volume
            'volume_lookback': 60,        # 60-bar volume average
            # ATR vol filter
            'atr_period': 20,
            'atr_lookback': 60,
            'max_atr_zscore': 1.5,        # Don't trade in vol spikes
            # Trade management
            'max_hold_bars': 30,          # Max 30 min hold
            'stop_loss_pct': 0.50,        # 0.5% stop
            'take_profit_pct': 0.35,      # 0.35% target
            # Cooldown
            'cooldown_bars': 180,         # 3-hour minimum between trades
        }
    }


def process_time_step(ctx):
    """
    Breakout fade with VWAP extension filter.

    Only fade breakouts when price is already significantly extended from VWAP.
    This creates a double confirmation: VWAP mean reversion + breakout rejection.
    """
    key = ('tETHUSD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']
    params = ctx['parameters']
    state = ctx['state']

    range_period = params['range_period']
    vwap_period = params['vwap_period']
    vwap_threshold = params['vwap_threshold']
    volume_spike = params['volume_spike']
    volume_lookback = params['volume_lookback']
    atr_period = params['atr_period']
    atr_lookback = params['atr_lookback']
    max_zscore = params['max_atr_zscore']
    max_hold = params['max_hold_bars']
    cooldown = params['cooldown_bars']

    # Initialize state
    if 'last_trade_bar' not in state:
        state['last_trade_bar'] = -cooldown
    if 'atr_vals' not in state:
        # Precompute ATR once at startup
        all_highs = [b.high for b in bars]
        all_lows = [b.low for b in bars]
        all_closes = [b.close for b in bars]
        state['atr_vals'] = atr(all_highs, all_lows, all_closes, atr_period)

    # Need enough data
    min_data = max(range_period + 1, atr_lookback + atr_period, volume_lookback, vwap_period)
    if i < min_data:
        return []

    bar = bars[i]

    # Compute range (prior bars, excluding current)
    range_high = max(bars[j].high for j in range(i - range_period, i))
    range_low = min(bars[j].low for j in range(i - range_period, i))

    # Get precomputed ATR
    atr_values = state['atr_vals']
    current_atr = atr_values[i] if i < len(atr_values) and atr_values[i] is not None else None
    if current_atr is None or current_atr < 0.01:
        return []

    # ATR z-score calculation
    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) < 20:
        return []

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

    # Compute volume ratio
    volumes = [bars[j].volume for j in range(max(0, i - volume_lookback), i)]
    avg_volume = sum(volumes) / len(volumes) if volumes else 1
    vol_ratio = bar.volume / avg_volume if avg_volume > 0 else 0

    # Compute VWAP
    vwap_val = vwap(bars, vwap_period, i)
    if vwap_val is None:
        return []

    # VWAP deviation in ATR units
    vwap_dev = (bar.close - vwap_val) / current_atr

    actions = []

    # === 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

        # VWAP reversion exit (target reached)
        if pos.side == 'long' and bar.close >= vwap_val:
            should_exit = True
        elif pos.side == 'short' and bar.close <= vwap_val:
            should_exit = True

        # Vol spike emergency exit
        if atr_zscore > 2.0:
            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': 'tETHUSD',
                'exchange': 'bitfinex',
            })
        return actions

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

    # No trades in vol spikes
    if atr_zscore > max_zscore:
        return []

    # Detect false breakout patterns
    false_breakup = (bar.high > range_high and bar.close < range_high)
    false_breakdown = (bar.low < range_low and bar.close > range_low)

    # Volume confirmation
    has_volume = vol_ratio >= volume_spike

    # VWAP extension filters
    extended_below_vwap = vwap_dev < -vwap_threshold
    extended_above_vwap = vwap_dev > vwap_threshold

    # Long: false breakdown + extended below VWAP (strong mean reversion setup)
    if false_breakdown and has_volume and extended_below_vwap:
        state['last_trade_bar'] = i
        actions.append({
            'action': 'open_long',
            'symbol': 'tETHUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
            'stop_loss_pct': params['stop_loss_pct'],
            'take_profit_pct': params['take_profit_pct'],
        })
    # Short: false breakup + extended above VWAP
    elif false_breakup and has_volume and extended_above_vwap:
        state['last_trade_bar'] = i
        actions.append({
            'action': 'open_short',
            'symbol': 'tETHUSD',
            'exchange': 'bitfinex',
            'size': 1.0,
            'stop_loss_pct': params['stop_loss_pct'],
            'take_profit_pct': params['take_profit_pct'],
        })

    return actions


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

    print("\n" + "="*60)
    print("Testing: breakout_fade_eth_vwap_extreme")
    print("Role: breakout_fade")
    print("Symbol: tETHUSD @ 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
    print(f"\n  MONTHLY RETURNS:")
    months = ['Jul', 'Aug', 'Sep', 'Oct', 'Nov']
    for idx, ret in enumerate(monthly_returns):
        print(f"    {months[idx]}: {ret:+.2f}%")

    positive_months = sum(1 for r in monthly_returns if r > 0)
    total_ret = overall.get('return', 0)
    print(f"\n  MONTHLY CONSISTENCY:")
    print(f"  Positive months: {positive_months}/5")

    # Cost analysis
    trades_count = overall.get('trades', 0)
    avg_trade = overall.get('avg_trade', 0)
    SLIPPAGE = 0.05
    total_cost = trades_count * 2 * SLIPPAGE
    after_slippage = total_ret - total_cost

    print(f"\n  COST ANALYSIS:")
    print(f"  Gross return: {total_ret:+.2f}%")
    print(f"  Slippage cost ({trades_count} trades x 0.10%): {total_cost:.2f}%")
    print(f"  After slippage: {after_slippage:+.2f}%")
    print(f"  Avg trade: {avg_trade:.3f}%")
    print(f"  Avg trade vs slippage: {'PASS' if avg_trade > 0.05 else 'FAIL'}")

    # Validation requirements
    print(f"\n  BREAKOUT_FADE VALIDATION:")
    sharpe_pass = overall.get('sharpe', 0) >= 0.4
    dd_pass = overall.get('max_dd', 0) < 18
    trades_pass = overall.get('trades', 0) >= 15
    loss_pass = overall.get('return', 0) > -8

    print(f"  - Sharpe >= 0.4: {'PASS' if sharpe_pass else 'FAIL'} ({overall.get('sharpe', 0):.2f})")
    print(f"  - DD < 18%: {'PASS' if dd_pass else 'FAIL'} ({overall.get('max_dd', 0):.1f}%)")
    print(f"  - Trades >= 15: {'PASS' if trades_pass else 'FAIL'} ({overall.get('trades', 0)})")
    print(f"  - Return > -8%: {'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'}")