← Back to list

doge_volatility_expansion_breakout VALIDATED PASS

Auto-discovered strategy

Symbol: DOGE | Exchange: Bitfinex | Role: momentum

3/6
Profitable Years
+40.7%
Total Return
24.4%
Avg Win Rate
-0.79
Avg Sharpe

Year-by-Year Results

Click a year to view chart

Year Return Win Rate Trades Max DD Sharpe
2020 +0.0% 0.0% 0 0.0% 0.00
2021 -15.7% 22.2% 9 19.1% -0.84
2022 +36.8% 37.5% 16 27.3% 1.06
2023 -68.5% 5.3% 19 50.4% -7.32
2024 +58.3% 42.9% 21 18.5% 1.43
2025 +29.9% 38.5% 13 20.3% 0.94

Performance Chart

Loading chart...

Walk-Forward Validation PASS

1/1 Windows Profitable
+5.0% OOS Return
0.00 Median Sharpe
0.000 Score
Window Train Period Val Period Val Return Val Test Period Test Return Status
WF-1 2024-01→2025-06 2025-07→2025-12 +5.0% OK 2026-01→ongoing +0.0% PASS

AI Review

Not yet reviewed. Run: ./review_strategy.sh doge_volatility_expansion_breakout

Source Code

"""
DOGE Volatility Expansion Breakout Strategy
============================================

A momentum strategy that capitalizes on volatility expansion after compression periods.

Core Concept:
- Identifies periods of volatility compression using ATR and Bollinger Band percentiles
- Waits for volatility to expand (breakout from squeeze)
- Enters long on breakout above recent highs with trend confirmation
- Uses trend filter (EMA20 > EMA50) to stay on the right side of the market

Entry Conditions:
1. Recent volatility squeeze: ATR or BB width in bottom 20th percentile within last 10 bars
2. Volatility expanding: Current ATR or BB percentile above 35th percentile
3. Uptrend: EMA(20) > EMA(50)
4. Price above EMA(20)
5. Bullish price action: Green candle
6. Volume confirmation: Above 20-bar average
7. Breakout: Price breaking 20-bar high

Exit Conditions:
1. Take profit: +15%
2. Stop loss: -5%
3. Signal exit: Price closes below EMA(20) for 2 consecutive bars

Risk Management:
- 5-bar cooldown between trades to avoid clustering
- 3:1 reward/risk ratio (15% TP vs 5% SL)
- Trend filter keeps us out of downtrends

Performance (TRAIN: 2024-01 to 2025-06):
- Total return: +81.6%
- Win rate: 46.4%
- Profit factor: 2.34
- Max drawdown: 18.5%
- Sharpe ratio: 1.75
- 28 trades
"""

import sys
sys.path.insert(0, '/root/trade_rules')
from lib import ema, sma, atr, bollinger_bands


def init_strategy():
    """Initialize strategy configuration."""
    return {
        'name': 'doge_volatility_expansion_breakout',
        'role': 'momentum',  # Long-only momentum, can lose in bear markets
        'warmup': 100,  # Need 100 bars for all indicators (50-bar percentile window + 50 buffer)
        'subscriptions': [
            {'symbol': 'tDOGE:USD', 'exchange': 'bitfinex', 'timeframe': '4h'},
        ],
        'parameters': {}
    }


# Module-level state for indicator caching
_indicators = {}
_last_trade_bar = [None]


def _calculate_indicators(bars):
    """Pre-calculate all indicators for the bar series."""
    global _indicators

    closes = [b.close for b in bars]
    highs = [b.high for b in bars]
    lows = [b.low for b in bars]
    volumes = [b.volume for b in bars]

    # ATR calculation (14-period)
    atr_vals = atr(highs, lows, closes, 14)

    # ATR as % of price (normalized volatility)
    atr_pct = [None] * len(bars)
    for i in range(len(bars)):
        if atr_vals[i] is not None and closes[i] > 0:
            atr_pct[i] = atr_vals[i] / closes[i] * 100

    # ATR percentile within 50-bar rolling window
    window = 50
    atr_pctile = [None] * len(bars)
    for i in range(window, len(bars)):
        if atr_pct[i] is None:
            continue
        recent = [v for v in atr_pct[i-window:i] if v is not None]
        if recent:
            rank = sum(1 for v in recent if v <= atr_pct[i])
            atr_pctile[i] = rank / len(recent) * 100

    # Bollinger Bands (20-period, 2 std)
    mid, upper, lower = bollinger_bands(closes, 20, 2.0)

    # BB width as % of middle band
    bb_width = [None] * len(bars)
    for i in range(len(bars)):
        if upper[i] is not None and lower[i] is not None and mid[i] and mid[i] > 0:
            bb_width[i] = (upper[i] - lower[i]) / mid[i] * 100

    # BB width percentile within 50-bar rolling window
    bb_pctile = [None] * len(bars)
    for i in range(window, len(bars)):
        if bb_width[i] is None:
            continue
        recent = [v for v in bb_width[i-window:i] if v is not None]
        if recent:
            rank = sum(1 for v in recent if v <= bb_width[i])
            bb_pctile[i] = rank / len(recent) * 100

    # EMAs for trend filter
    ema20 = ema(closes, 20)
    ema50 = ema(closes, 50)

    # Volume SMA for confirmation
    vol_sma = sma(volumes, 20)

    _indicators = {
        'closes': closes,
        'highs': highs,
        'lows': lows,
        'volumes': volumes,
        'atr_pctile': atr_pctile,
        'bb_pctile': bb_pctile,
        'ema20': ema20,
        'ema50': ema50,
        'vol_sma': vol_sma,
    }
    return _indicators


def process_time_step(ctx):
    """
    Process each time step and return trading actions.

    Args:
        ctx: Context dict with bars, positions, parameters, etc.

    Returns:
        List of action dicts (open_long, close_long, etc.)
    """
    key = ('tDOGE:USD', 'bitfinex')
    bars = ctx['bars'][key]
    i = ctx['i']
    positions = ctx['positions']

    global _indicators, _last_trade_bar

    # Recalculate indicators if bars changed
    if not _indicators or len(_indicators['closes']) != len(bars):
        _calculate_indicators(bars)
        _last_trade_bar = [None]

    ind = _indicators
    actions = []

    # Parameters (round numbers only - no curve-fitting)
    squeeze_lookback = 10     # Look for squeeze in last N bars
    squeeze_threshold = 20    # Bottom 20th percentile = squeeze
    expansion_threshold = 35  # Must expand above 35th percentile
    cooldown_bars = 5         # Wait 5 bars (20 hours) between trades
    breakout_period = 20      # 20-bar high for breakout

    # =========================================================================
    # EXIT LOGIC
    # =========================================================================
    if key in positions:
        # Exit if price closes below EMA20 for 2 consecutive bars
        if ind['ema20'][i] is not None and ind['closes'][i] < ind['ema20'][i]:
            if i > 0 and ind['closes'][i-1] < ind['ema20'][i-1]:
                actions.append({
                    'action': 'close_long',
                    'symbol': 'tDOGE:USD',
                    'exchange': 'bitfinex',
                })
                _last_trade_bar[0] = i
                return actions

        return actions

    # =========================================================================
    # ENTRY LOGIC
    # =========================================================================

    # Cooldown check - avoid trade clustering
    if _last_trade_bar[0] is not None and i - _last_trade_bar[0] < cooldown_bars:
        return actions

    # Condition 1: Recent volatility squeeze (ATR or BB in bottom 20%)
    had_squeeze = False
    for lookback in range(1, squeeze_lookback + 1):
        idx = i - lookback
        if idx < 0:
            continue
        atr_squeezed = ind['atr_pctile'][idx] is not None and ind['atr_pctile'][idx] < squeeze_threshold
        bb_squeezed = ind['bb_pctile'][idx] is not None and ind['bb_pctile'][idx] < squeeze_threshold
        if atr_squeezed or bb_squeezed:
            had_squeeze = True
            break

    if not had_squeeze:
        return actions

    # Condition 2: Volatility is now expanding (above 35th percentile)
    atr_expanding = ind['atr_pctile'][i] is not None and ind['atr_pctile'][i] > expansion_threshold
    bb_expanding = ind['bb_pctile'][i] is not None and ind['bb_pctile'][i] > expansion_threshold
    if not (atr_expanding or bb_expanding):
        return actions

    # Condition 3: Uptrend filter (EMA20 > EMA50)
    if ind['ema20'][i] is None or ind['ema50'][i] is None:
        return actions
    if ind['ema20'][i] <= ind['ema50'][i]:
        return actions

    # Condition 4: Price above EMA20
    if ind['closes'][i] < ind['ema20'][i]:
        return actions

    # Condition 5: Green candle (bullish)
    if ind['closes'][i] <= ind['closes'][i-1]:
        return actions

    # Condition 6: Volume above 20-bar average
    if ind['vol_sma'][i] is None or ind['volumes'][i] < ind['vol_sma'][i]:
        return actions

    # Condition 7: Breaking 20-bar high
    if i < breakout_period:
        return actions
    prev_high = max(ind['highs'][i-breakout_period:i])
    if ind['highs'][i] <= prev_high:
        return actions

    # All conditions met - open long position
    actions.append({
        'action': 'open_long',
        'symbol': 'tDOGE:USD',
        'exchange': 'bitfinex',
        'size': 1.0,
        'stop_loss_pct': 5.0,     # 5% stop loss
        'take_profit_pct': 15.0,  # 15% take profit (3:1 R:R)
    })
    _last_trade_bar[0] = i

    return actions


# For standalone testing
if __name__ == '__main__':
    from strategy import backtest_strategy

    # Reset state
    _indicators = {}
    _last_trade_bar = [None]

    print("Testing doge_volatility_expansion_breakout strategy...")
    results, profitable, _ = backtest_strategy(init_strategy, process_time_step)