Auto-discovered strategy
Symbol: BTC | Exchange: Bitfinex | Role: momentum
Click a year to view chart
| Year | Return | Win Rate | Trades | Max DD | Sharpe |
|---|---|---|---|---|---|
| 2020 | +37.4% | 43.8% | 16 | 7.8% | 1.53 |
| 2021 | +38.9% | 53.3% | 15 | 5.9% | 1.54 |
| 2022 | -13.7% | 23.1% | 13 | 16.0% | -2.04 |
| 2023 | +21.5% | 37.5% | 16 | 6.8% | 1.03 |
| 2024 | +30.8% | 47.6% | 21 | 7.9% | 1.47 |
| 2025 | +6.5% | 50.0% | 12 | 4.3% | 0.64 |
| Window | Train Period | Val Period | Val Return | Val | Test Period | Test Return | Status |
|---|---|---|---|---|---|---|---|
| WF-1 | 2024-01→2025-06 | 2025-07→2025-12 | +4.2% | OK | 2026-01→ongoing | +0.0% | PASS |
Not yet reviewed. Run: ./review_strategy.sh volume_absorption_momentum
"""
Volume Absorption Momentum Strategy
====================================
A volume spike pattern strategy that identifies institutional absorption of
selling pressure during uptrends, then enters on breakout confirmation.
Concept:
--------
When volume spikes while price holds above key support (EMA20), it often
indicates institutions absorbing supply. The follow-through confirmation
(next bar closes above spike high) validates the absorption pattern and
signals continuation.
Entry Conditions:
-----------------
1. REGIME FILTER: EMA20 > EMA50 and close > EMA100 (confirmed uptrend)
2. Volume spike: 1.2x the 20-bar average (institutional activity)
3. Support held: bar low remains above EMA20
4. Confirmation: next bar closes above the spike bar's high
Exit Conditions:
----------------
1. Price closes below EMA15 (momentum fading)
2. 3% stop loss (built-in position protection)
Risk Management:
----------------
- Regime filter prevents trading when trend is unclear
- Quick exit on EMA15 break locks in gains early
- 3% stop loss limits downside per trade
Train Performance (2024-2025H1):
--------------------------------
2024: +30.8% | 21 trades | 48% WR
2025: +1.2% | 8 trades | 38% WR
Total: +32.0% | Max DD: 7.9%
Validation (2025-H2 - UNSEEN):
------------------------------
+4.2% | Sharpe: 0.68 | PASSED
"""
def init_strategy():
"""Initialize strategy configuration."""
return {
'name': 'volume_absorption_momentum',
'role': 'momentum',
'warmup': 200,
'subscriptions': [
{'symbol': 'tBTCUSD', 'exchange': 'bitfinex', 'timeframe': '4h'},
],
'parameters': {
'vol_mult': 1.2,
'vol_lookback': 20,
'ema_exit': 15,
'ema_short': 20,
'ema_long': 50,
'ema_regime': 100,
'stop_loss_pct': 3.0,
}
}
def process_time_step(ctx):
"""
Process each time step for volume absorption pattern detection.
Args:
ctx: Context dict containing bars, positions, state, parameters
Returns:
List of action dicts (open_long, close_long, etc.)
"""
from lib import ema
key = ('tBTCUSD', 'bitfinex')
bars = ctx['bars'][key]
i = ctx['i']
positions = ctx['positions']
state = ctx['state']
params = ctx['parameters']
# Extract parameters (using round numbers)
vol_mult = params.get('vol_mult', 1.2)
vol_lookback = params.get('vol_lookback', 20)
ema_exit = params.get('ema_exit', 15)
ema_short = params.get('ema_short', 20)
ema_long = params.get('ema_long', 50)
ema_regime = params.get('ema_regime', 100)
stop_loss_pct = params.get('stop_loss_pct', 3.0)
# Build indicator arrays
closes = [b.close for b in bars[:i+1]]
volumes = [b.volume for b in bars[:i+1]]
highs = [b.high for b in bars[:i+1]]
lows = [b.low for b in bars[:i+1]]
ema_exit_vals = ema(closes, ema_exit)
ema_short_vals = ema(closes, ema_short)
ema_long_vals = ema(closes, ema_long)
ema_regime_vals = ema(closes, ema_regime)
actions = []
# Safety checks
if i < ema_regime + 5:
return actions
if ema_short_vals[-1] is None or ema_long_vals[-1] is None:
return actions
if ema_regime_vals[-1] is None or ema_exit_vals[-1] is None:
return actions
# REGIME FILTER: EMA20 > EMA50 and close > EMA100
uptrend = (
ema_short_vals[-1] > ema_long_vals[-1] and
closes[-1] > ema_regime_vals[-1]
)
if key not in positions:
# Only enter in confirmed uptrends
if not uptrend:
return actions
# Initialize state for tracking absorption setups
if 'pending_absorption' not in state:
state['pending_absorption'] = None
# Check previous bar for volume spike pattern
vol_avg = sum(volumes[i-vol_lookback:i]) / vol_lookback
prev_vol = volumes[i-1]
if prev_vol > vol_avg * vol_mult:
prev_bar = bars[i-1]
bar_range = prev_bar.high - prev_bar.low
# Support held: bar low above EMA20
if prev_bar.low > ema_short_vals[-2] and bar_range > 0:
state['pending_absorption'] = {
'bar_idx': i-1,
'high': prev_bar.high,
'low': prev_bar.low,
}
# Check for confirmation on current bar
if state['pending_absorption'] is not None:
spike_high = state['pending_absorption']['high']
current_bar = bars[i]
# Confirmation: close above spike high
if current_bar.close > spike_high:
actions.append({
'action': 'open_long',
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
'size': 1.0,
'stop_loss_pct': stop_loss_pct,
})
# Reset pending after checking (whether entered or not)
state['pending_absorption'] = None
else:
# Position management: exit when price closes below EMA15
if closes[-1] < ema_exit_vals[-1]:
actions.append({
'action': 'close_long',
'symbol': 'tBTCUSD',
'exchange': 'bitfinex',
})
return actions