Skip to content
Back to App

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", "")
# Parameters
ENTRY_PERIOD = 20 # Breakout lookback: enter on 20-bar high/low
EXIT_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 = None
entry_price = 0.0
trade_count = 0
wins = 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:

  1. Current High - Current Low
  2. |Current High - Previous Close|
  3. |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.value

Strategy 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", "")
# Parameters
ATR_PERIOD = 14
ATR_MULTIPLIER = 2.0 # Breakout threshold: price must move 2x ATR
MA_PERIOD = 20 # Reference point for breakout
SL_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 = None
entry_price = 0.0
stop_loss = 0.0
trade_count = 0
wins = 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 average
if bar["h"] > upper and bar["v"] > avg_volume * 1.5:
# High-confidence breakout
pass

2. 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 level
if bar["c"] > upper:
pass

3. Multi-Bar Confirmation

Wait for N consecutive closes above the level:

bars_above = 0
for j in range(-3, 0):
if closes[j] > upper:
bars_above += 1
# Only enter after 2 consecutive closes above
if bars_above >= 2 and position is None:
pass

4. ATR Filter

Reject breakouts in low-volatility environments where whipsaws are more common:

# Only trade breakouts when ATR is above its own average
atr_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
pass

Donchian Channel Period Selection

PeriodCharacterBest For
10Short-range breakout, many signalsScalping on 1m bars
20Standard (Turtle system), balancedIntraday on 1-5m bars
50Medium-term, catches bigger movesSwing on 15m-1h bars
100Long-term, very few signalsMulti-day trends

What’s Next