Breakout Trading
Breakout strategies aim to capture the explosive moves that occur when price breaks through established support or resistance levels. In this tutorial, you will build channel breakout and volatility breakout strategies, and learn techniques for filtering out false breakouts.
What Is a Breakout?
A breakout occurs when price moves above a resistance level or below a support level with enough momentum to sustain the move. The idea is simple: if price has been contained in a range and it finally breaks out, it is likely to continue in that direction.
Key concepts:
- Resistance: A price level where selling pressure has repeatedly stopped upward movement
- Support: A price level where buying pressure has repeatedly stopped downward movement
- Range: The area between support and resistance where price has been consolidating
Donchian Channel Breakout
The Donchian Channel is the simplest breakout system. It tracks the highest high and lowest low over N bars:
- Upper band: Highest high of last N bars
- Lower band: Lowest low of last N bars
- Buy signal: Price breaks above the upper band
- Sell signal: Price breaks below the lower band
This was the core of the famous Turtle Trading system.
def donchian_upper(highs, period): """Highest high of last `period` bars (excluding current).""" if len(highs) < period + 1: return None return max(highs[-(period + 1):-1])
def donchian_lower(lows, period): """Lowest low of last `period` bars (excluding current).""" if len(lows) < period + 1: return None return min(lows[-(period + 1):-1])Strategy 1: Donchian Channel Breakout
#!/usr/bin/env python3"""Donchian Channel Breakout Strategy — TestMax Algo Playground"""import os, time
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))CONTRACT_ID = os.environ.get("CONTRACT_ID", "")TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))SPEED_FILE = os.environ.get("SPEED_FILE", "")
# ParametersENTRY_PERIOD = 20 # Breakout lookback: enter on 20-bar high/lowEXIT_PERIOD = 10 # Exit lookback: exit on 10-bar opposite break
print(f"[INFO] Strategy: Donchian Breakout (Entry: {ENTRY_PERIOD}, Exit: {EXIT_PERIOD})")print("-" * 50)
def donchian_upper(highs, period): if len(highs) < period + 1: return None return max(highs[-(period + 1):-1])
def donchian_lower(lows, period): if len(lows) < period + 1: return None return min(lows[-(period + 1):-1])
highs, lows, closes = [], [], []position = Noneentry_price = 0.0trade_count = 0wins = 0
try: for i in range(TOTAL_BARS): bar = get_next_bar() if bar is None: break read_speed() if STEP_DELAY > 0: time.sleep(STEP_DELAY)
highs.append(bar["h"]) lows.append(bar["l"]) closes.append(bar["c"]) price = bar["c"]
upper = donchian_upper(highs, ENTRY_PERIOD) lower = donchian_lower(lows, ENTRY_PERIOD) exit_upper = donchian_upper(highs, EXIT_PERIOD) exit_lower = donchian_lower(lows, EXIT_PERIOD)
if upper is None or lower is None: continue
# --- Entry: breakout above/below channel --- if position is None: if bar["h"] > upper: result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) if result.get("orderId"): position = "LONG" entry_price = price print(f"[BUY] Bar {i} @ {price:.2f} | Broke above {upper:.2f}")
elif bar["l"] < lower: result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) if result.get("orderId"): position = "SHORT" entry_price = price print(f"[SELL] Bar {i} @ {price:.2f} | Broke below {lower:.2f}")
# --- Exit: opposite channel break (shorter period) --- elif position == "LONG" and exit_lower is not None and bar["l"] < exit_lower: result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) if result.get("orderId"): pnl = price - entry_price trade_count += 1 if pnl > 0: wins += 1 print(f"[EXIT] Bar {i} @ {price:.2f} | P&L: {pnl:+.2f} pts") position = None
elif position == "SHORT" and exit_upper is not None and bar["h"] > exit_upper: result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) if result.get("orderId"): pnl = entry_price - price trade_count += 1 if pnl > 0: wins += 1 print(f"[EXIT] Bar {i} @ {price:.2f} | P&L: {pnl:+.2f} pts") position = None
finally: try: close_all_positions(ACCOUNT_ID, CONTRACT_ID) except: pass account = get_account(ACCOUNT_ID) balance = account["balance"] if account else 50000 print("-" * 50) print(f"[DONE] Balance: ${balance:,.2f} | P&L: ${balance - 50000:+,.2f}") print(f"[DONE] Trades: {trade_count} | Win Rate: {wins/trade_count*100:.1f}%" if trade_count else "[DONE] No trades")Average True Range (ATR)
Before building the volatility breakout strategy, you need the Average True Range indicator. ATR measures volatility by averaging the “true range” of each bar over N periods.
True Range is the largest of:
- Current High - Current Low
- |Current High - Previous Close|
- |Current Low - Previous Close|
Cases 2 and 3 account for gaps between bars.
class ATR: def __init__(self, period=14): self.period = period self.value = None self._prev_close = None self._tr_buffer = []
def update(self, high, low, close): if self._prev_close is not None: tr1 = high - low tr2 = abs(high - self._prev_close) tr3 = abs(low - self._prev_close) true_range = max(tr1, tr2, tr3)
if self.value is None: self._tr_buffer.append(true_range) if len(self._tr_buffer) == self.period: self.value = sum(self._tr_buffer) / self.period self._tr_buffer = None else: # Wilder's smoothing self.value = (self.value * (self.period - 1) + true_range) / self.period
self._prev_close = close return self.valueStrategy 2: ATR Volatility Breakout
Instead of fixed channel levels, this strategy uses ATR to set dynamic breakout thresholds. Buy when price moves more than 2x ATR above a reference point (like a moving average). This adapts to the current volatility — wider thresholds in volatile markets, tighter in quiet ones.
#!/usr/bin/env python3"""ATR Volatility Breakout Strategy — TestMax Algo Playground"""import os, time
ACCOUNT_ID = int(os.environ.get("ACCOUNT_ID", "0"))CONTRACT_ID = os.environ.get("CONTRACT_ID", "")TOTAL_BARS = int(os.environ.get("TOTAL_BARS", "5000"))STEP_DELAY = float(os.environ.get("STEP_DELAY", "0.02"))SPEED_FILE = os.environ.get("SPEED_FILE", "")
# ParametersATR_PERIOD = 14ATR_MULTIPLIER = 2.0 # Breakout threshold: price must move 2x ATRMA_PERIOD = 20 # Reference point for breakoutSL_ATR_MULT = 1.5 # Stop loss at 1.5x ATR
print(f"[INFO] Strategy: ATR Volatility Breakout (ATR {ATR_PERIOD}, Mult {ATR_MULTIPLIER}x)")print("-" * 50)
# (Include ATR class from above)
class EMA: def __init__(self, period): self.period = period self.multiplier = 2 / (period + 1) self.value = None self._buffer = [] def update(self, price): if self.value is None: self._buffer.append(price) if len(self._buffer) == self.period: self.value = sum(self._buffer) / self.period self._buffer = None else: self.value = (price - self.value) * self.multiplier + self.value return self.value
atr = ATR(ATR_PERIOD)ema = EMA(MA_PERIOD)position = Noneentry_price = 0.0stop_loss = 0.0trade_count = 0wins = 0
try: for i in range(TOTAL_BARS): bar = get_next_bar() if bar is None: break read_speed() if STEP_DELAY > 0: time.sleep(STEP_DELAY)
price = bar["c"] atr_val = atr.update(bar["h"], bar["l"], price) ema_val = ema.update(price)
if atr_val is None or ema_val is None: continue
upper_band = ema_val + (atr_val * ATR_MULTIPLIER) lower_band = ema_val - (atr_val * ATR_MULTIPLIER)
# --- Stop loss check --- if position == "LONG" and bar["l"] <= stop_loss: result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) if result.get("orderId"): pnl = stop_loss - entry_price trade_count += 1 print(f"[SL] Bar {i} @ {stop_loss:.2f} | P&L: {pnl:+.2f} pts") position = None
elif position == "SHORT" and bar["h"] >= stop_loss: result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) if result.get("orderId"): pnl = entry_price - stop_loss trade_count += 1 print(f"[SL] Bar {i} @ {stop_loss:.2f} | P&L: {pnl:+.2f} pts") position = None
# --- Entry: price breaks above/below ATR bands --- if position is None: if bar["h"] > upper_band: result = place_order(ACCOUNT_ID, CONTRACT_ID, 0, 1) if result.get("orderId"): position = "LONG" entry_price = price stop_loss = price - (atr_val * SL_ATR_MULT) print(f"[BUY] Bar {i} @ {price:.2f} | ATR: {atr_val:.2f} | SL: {stop_loss:.2f}")
elif bar["l"] < lower_band: result = place_order(ACCOUNT_ID, CONTRACT_ID, 1, 1) if result.get("orderId"): position = "SHORT" entry_price = price stop_loss = price + (atr_val * SL_ATR_MULT) print(f"[SELL] Bar {i} @ {price:.2f} | ATR: {atr_val:.2f} | SL: {stop_loss:.2f}")
# --- Trailing stop (move SL in direction of profit) --- elif position == "LONG": new_sl = price - (atr_val * SL_ATR_MULT) if new_sl > stop_loss: stop_loss = new_sl # Ratchet up, never down
elif position == "SHORT": new_sl = price + (atr_val * SL_ATR_MULT) if new_sl < stop_loss: stop_loss = new_sl # Ratchet down, never up
finally: try: close_all_positions(ACCOUNT_ID, CONTRACT_ID) except: pass account = get_account(ACCOUNT_ID) balance = account["balance"] if account else 50000 print("-" * 50) print(f"[DONE] Balance: ${balance:,.2f} | P&L: ${balance - 50000:+,.2f}") print(f"[DONE] Trades: {trade_count} | Win Rate: {wins/trade_count*100:.1f}%" if trade_count else "[DONE] No trades")Handling False Breakouts
False breakouts are the biggest challenge. Price breaks above resistance, you buy, then it immediately reverses. Here are proven filters:
1. Volume Confirmation
Only trade breakouts with above-average volume:
avg_volume = sum(volumes[-20:]) / 20 if len(volumes) >= 20 else 0
# Only enter if volume is at least 1.5x averageif bar["h"] > upper and bar["v"] > avg_volume * 1.5: # High-confidence breakout pass2. Close Confirmation
Require the bar to close above/below the level, not just wick through it:
# Weak: any touch triggers entry (more false breakouts)if bar["h"] > upper: pass
# Strong: only enter if the bar CLOSES above the levelif bar["c"] > upper: pass3. Multi-Bar Confirmation
Wait for N consecutive closes above the level:
bars_above = 0for j in range(-3, 0): if closes[j] > upper: bars_above += 1
# Only enter after 2 consecutive closes aboveif bars_above >= 2 and position is None: pass4. ATR Filter
Reject breakouts in low-volatility environments where whipsaws are more common:
# Only trade breakouts when ATR is above its own averageatr_values.append(atr_val)if len(atr_values) >= 50: avg_atr = sum(atr_values[-50:]) / 50 if atr_val > avg_atr: # Volatility is expanding — breakouts more likely to follow through passDonchian Channel Period Selection
| Period | Character | Best For |
|---|---|---|
| 10 | Short-range breakout, many signals | Scalping on 1m bars |
| 20 | Standard (Turtle system), balanced | Intraday on 1-5m bars |
| 50 | Medium-term, catches bigger moves | Swing on 15m-1h bars |
| 100 | Long-term, very few signals | Multi-day trends |
What’s Next
- Risk Management in Code — add stop losses, daily limits, and position sizing to your breakout strategy
- Multi-Indicator Strategies — combine breakout signals with trend and momentum filters