DEV Community

isabelle dubuis
isabelle dubuis

Posted on

ICT Order Block + FVG strategy in Pine Script v5: backtest results + full code

I've been backtesting an ICT-style Order Block + Fair Value Gap strategy on crypto and forex. After 50 iterations and a lot of dead variants, here's what survived. Posting the Pine Script v5 below in case it saves someone the same week of debugging.

The setup

ICT (Inner Circle Trader) methodology relies on two ideas:

  1. Order Block (OB) — the last opposite candle right before a strong displacement. Bull OB = last bearish candle before a sharp bullish push.
  2. Fair Value Gap (FVG) — a 3-bar imbalance where price moved so fast it left a gap (low > high[2]).

Entry = price retraces back into the OB, ideally inside an FVG, with trend confirmation.

Why this version finally worked

Earlier tries took every OB. Win rate dropped to 35%. Two filters fixed it:

  • Displacement filter: the move out of the OB must exceed 2 × ATR(14). Weak moves create weak OBs. They don't hold.
  • Trend filter: only longs above EMA(200) trending up. Short below, trending down. Sounds basic, but cut my losing trades by 40%.

Stop loss = OB low minus 0.3 × ATR. TP = 3R fixed. Anything tighter and noise eats you. Anything wider and you give back too much.

The full Pine Script v5

//@version=5
strategy("ICT_OB_SEED11", overlay=true, initial_capital=3000, commission_type=strategy.commission.percent, commission_value=0.05, slippage=2, default_qty_type=strategy.cash, default_qty_value=300, process_orders_on_close=true)

displ_atr_mult = input.float(2.0, "Displacement ATR mult")
ob_lookback    = input.int(20, "OB lookback")

atr = ta.atr(14)
displ_up = close - close[1] > atr * displ_atr_mult
displ_dn = close[1] - close > atr * displ_atr_mult

var float obUpHi = na, var float obUpLo = na
var float obDnHi = na, var float obDnLo = na

if displ_up and close[1] < open[1]
    obUpHi := high[1]
    obUpLo := low[1]
if displ_dn and close[1] > open[1]
    obDnHi := high[1]
    obDnLo := low[1]

bullFVG = low > high[2]
bearFVG = high < low[2]

ema200 = ta.ema(close, 200)
upTrend = close > ema200 and ema200 > ema200[20]
dnTrend = close < ema200 and ema200 < ema200[20]

retestLong  = upTrend and not na(obUpHi) and low <= obUpHi and high >= obUpLo and close > open
retestShort = dnTrend and not na(obDnLo) and high >= obDnLo and low <= obDnHi and close < open

if retestLong and barstate.isconfirmed and strategy.position_size == 0
    sl = obUpLo - atr * 0.3
    tp = close + (close - sl) * 3
    strategy.entry("L", strategy.long)
    strategy.exit("XL", "L", stop=sl, limit=tp)
    obUpHi := na
    obUpLo := na

if retestShort and barstate.isconfirmed and strategy.position_size == 0
    sl = obDnHi + atr * 0.3
    tp = close - (sl - close) * 3
    strategy.entry("S", strategy.short)
    strategy.exit("XS", "S", stop=sl, limit=tp)
    obDnHi := na
    obDnLo := na
Enter fullscreen mode Exit fullscreen mode

Results across pairs (1H, 2 years backtest)

  • BTCUSDT: PF 1.42, MaxDD -8.2%, 312 trades
  • EURUSD: PF 1.18, MaxDD -6.1%, 184 trades
  • XAUUSD: PF 1.61, MaxDD -11.4%, 247 trades
  • SP500 1H: PF 1.09, MaxDD -7.8%, 142 trades

PF below 1.1 = skip. Above 1.4 = keep monitoring forward. Forward test always trails backtest by 15-25%.

What I tried that didn't work

  • ATR(7) instead of ATR(14): too noisy, false displacements
  • Tighter SL (0.1 ATR): stopped out 70% of valid setups
  • 5R TP: too greedy, only 12% hit it before reversal
  • No trend filter: half the OBs bounced once then died

Next iterations

  • Add session filter (London/NY only, skip Asia)
  • Multi-timeframe OB confirmation (4H OB on 1H entry)
  • Volume profile overlay for OB validation

About me

I run a small portfolio of niche tools and content sites across AI, SEO, trading, and B2B. Some of them:

AI & automation:

SEO & sales:

Trust, security, finance:

Niche content:

If you trade and want to chat about backtesting frameworks or automation in general, drop a comment. Anyone running ICT on lower timeframes (5m/15m)? Curious what your filter stack looks like.

Top comments (0)