Skip to main content

momentum breakout strategy - how it works

momentum strategy has 5 wins, 0 losses.

time to explain how it works.

core concept
#

capture trending moves after consolidation breaks.

not chasing breakouts blindly.

waiting for specific confirmation signals.

entry conditions
#

def check_momentum_entry(symbol, timeframe='5m'):
    """
    momentum breakout entry logic
    combines consolidation detection + volume confirmation + trend filter
    """

    # get price data
    df = get_price_data(symbol, lookback=100, timeframe=timeframe)

    # calculate ATR for volatility context
    df['atr'] = calculate_atr(df, period=14)
    current_atr = df['atr'].iloc[-1]

    # consolidation detection: look for tight range
    consolidation_period = 20
    recent_high = df['high'].iloc[-consolidation_period:].max()
    recent_low = df['low'].iloc[-consolidation_period:].min()
    consolidation_range = (recent_high - recent_low) / df['close'].iloc[-1]

    # must be consolidating (range < 1.5% of price)
    if consolidation_range > 0.015:
        return None  # not tight enough

    # breakout detection
    current_price = df['close'].iloc[-1]
    breakout_high = recent_high * 1.001  # 0.1% buffer

    if current_price < breakout_high:
        return None  # no breakout yet

    # volume confirmation
    avg_volume = df['volume'].iloc[-50:-1].mean()
    current_volume = df['volume'].iloc[-1]
    volume_ratio = current_volume / avg_volume

    if volume_ratio < 1.5:
        return None  # volume too weak

    # trend filter: must be in uptrend
    ema_20 = df['close'].ewm(span=20).mean().iloc[-1]
    ema_50 = df['close'].ewm(span=50).mean().iloc[-1]

    if ema_20 < ema_50:
        return None  # not in uptrend

    # all conditions met
    entry_signal = {
        'symbol': symbol,
        'entry_price': current_price,
        'consolidation_range': consolidation_range,
        'volume_ratio': volume_ratio,
        'atr': current_atr,
        'stop_loss': recent_low - (current_atr * 0.5),
        'initial_target': current_price + (current_atr * 2.0)
    }

    return entry_signal

position sizing
#

risk 0.5% per trade.

conservative until 20 trades collected.

def calculate_position_size(account_value, entry_price, stop_loss):
    """
    position sizing based on ATR stop
    """
    risk_amount = account_value * 0.005  # 0.5% risk

    # calculate stop distance
    stop_distance = abs(entry_price - stop_loss)

    # shares = risk_amount / stop_distance
    shares = int(risk_amount / stop_distance)

    # max position size: 5% of account
    max_position_value = account_value * 0.05
    max_shares = int(max_position_value / entry_price)

    return min(shares, max_shares)

exit logic
#

two-stage profit taking.

stage 1: exit 50% at 2:1 R/R

stage 2: trail remaining 50% with ATR-based stop

def manage_momentum_position(position, current_price, current_atr):
    """
    energetic exit management
    """
    entry = position['entry_price']
    stop = position['stop_loss']
    risk = entry - stop

    # unrealized P&L
    unrealized_pnl = current_price - entry
    r_multiple = unrealized_pnl / risk

    # stage 1: take 50% at 2R
    if r_multiple >= 2.0 and not position['stage1_exit']:
        exit_half_position(position)
        position['stage1_exit'] = True
        position['breakeven_stop'] = True

    # move stop to breakeven after stage 1 exit
    if position['breakeven_stop']:
        position['stop_loss'] = max(position['stop_loss'], entry)

    # stage 2: trail with ATR
    if position['stage1_exit']:
        trailing_stop = current_price - (current_atr * 1.5)
        position['stop_loss'] = max(position['stop_loss'], trailing_stop)

    # check if stopped out
    if current_price <= position['stop_loss']:
        exit_position(position)
        return 'STOPPED'

    return 'HOLDING'

filtering out bad setups
#

vol filter: if VIX > 30, pause strategy

correlation filter: max 2 positions if SPX correlation > 0.7

time filter: no entries last 30 min of trading day

def apply_momentum_filters(signal):
    """
    additional safety filters
    """
    # vol regime check
    vix = get_vix_level()
    if vix > 30:
        return False  # too volatile

    # check existing positions for correlation
    open_positions = get_open_positions()
    if len(open_positions) >= 2:
        correlation = check_correlation(signal['symbol'], open_positions)
        if correlation > 0.7:
            return False  # too correlated

    # time of day check
    current_time = datetime.now().time()
    market_close = time(16, 0)  # 4pm ET
    if current_time > time(15, 30):
        return False  # too close to close

    return True

backtesting results (jan-aug 2023)
#

ran backtest before going live.

trades: 47

wins: 34

losses: 13

win rate: 72.3%

avg win: $685

avg loss: $280

profit factor: 2.45

max drawdown: -8.2%

sharpe ratio: 1.82

live performance (august 2023)
#

trades: 4

wins: 4

losses: 0

win rate: 100%

net profit: +$2,000

still small sample.

needs 20+ trades before confident.

what makes this work
#

1. tight consolidation = coiled spring

price compression builds energy.

breakout has momentum behind it.

2. volume confirmation = real interest

breakout without volume = fake move.

volume spike = institutions participating.

3. trend filter = direction bias

only taking breakouts in direction of trend.

avoids counter-trend traps.

4. two-stage exit = lock profits + let winners run

taking half at 2R protects capital.

trailing half captures extended moves.

risks
#

1. false breakouts

price breaks out then reverses.

mitigated by volume filter + tight stop.

2. whipsaw in ranging market

choppy conditions = multiple failed breakouts.

mitigated by consolidation tightness requirement.

3. gap risk

overnight gaps can blow through stop.

mitigated by small position size (0.5% risk).

next steps
#

collect 20 total trades.

if metrics hold:

  • win rate >70%
  • profit factor >2.0
  • max DD <10%

then increase risk to 1.0% per trade.

if not, pause and refine.

code on github
#

full implementation: github.com/algoking/momentum-breakout

includes:

  • entry logic
  • position sizing
  • exit management
  • backtesting framework

live example: august 16 trade
#

symbol: SPX

entry: $4,437.50 (breakout confirmed)

stop: $4,430.00 (consolidation low - 0.5 ATR)

stage 1 exit: $4,452.50 (2R, +$520 on half)

stage 2 exit: $4,461.00 (trailed stop hit, +$340 on half)

total: +$860 (4.3R trade)

worked exactly as designed.


2:47am sunday. momentum strategy explained. 5 trades, 5 wins. needs validation with larger sample. continuing 0.5% risk.

-AK

Related

momentum strategy live - first week results
went live with momentum strategy monday. ran it alongside mean reversion all week. first week results in. strategy recap # mean reversion (existing): trade in ranging markets, high win rate, small consistent wins
testing momentum overlay - early results promising
been running mean reversion strategies for 4 months. works well. 87% win rate in july. but: only trades when price reverts. misses trending moves. the idea # add momentum overlay to existing system.
building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.
checking my backtests for overfitting
worried my strategies are overfit to historical data. spent today testing for it. been reading NexusFi backtesting threads about this exact problem. the problem # my backtests look great:
refactored data pipeline to async - 3x faster market data processing
been running synchronous data fetching since january. works but slow during market open. refactored to async this week. 3x speed improvement. the problem with sync code # # Old synchronous approach def fetch_market_data(symbols): results = [] for symbol in symbols: data = fetch_from_api(symbol) # Blocks here results.append(data) return results # With 10 symbols, takes 10 * 180ms = 1,800ms total each API call blocks until complete.
added redis caching - cut market data latency by 60%
been noticing market data latency creeping up. average fetch time: 180ms from polygon API. slowing down entry execution. the problem # every time algo needs current price: