Auto-discovered strategy
Symbol: DOGE | Exchange: Bitfinex | Role: momentum
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 |
| 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 |
Not yet reviewed. Run: ./review_strategy.sh doge_volatility_expansion_breakout
"""
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)