A $2,000 trailing drawdown on the NQ opening range breakout, two years of MNQ
The drawdown is a block of inputs and books on top of the strategy: the floor trails the day's closing balance or every new high, stops at the starting balance or keeps rising, and is checked at each candle's worst point with the open trade included. When an account passes the target or hits the floor, the next starts the following session, so two years of data answer for every account in them, not only the first. The entries are an example, our NQ study's opening range breakout at $500 a trade; your own go in their place.
- Trades
- 475
- Won
- 44.4%
- Profit factor
- 1.01
- Net
- +2.1%
- Worst drawdown
- −19.6%
- Worst trade
- −$531
MNQ1! · 5-minute · 23 Oct 2024 → 22 Oct 2026 · 2 years · 517 sessions · $0.75 a contract each side (about a micro's all-in rate), one tick of slippage, $50,000 account, $500 risk a trade, one trade a day, flat by 15:55 New York
The entries made 2.1% over two years, +$1,033 on 475 trades, and the last six months, held back by the terminal, lost 15.9%. Kept as back-to-back accounts with a $3,000 target and a $2,000 drawdown trailing the day's close, the same trades started 22 accounts: 6 passed, 15 failed on the drawdown and one was still running when the data ran out. An account lasted from 6 to 66 trades, 18 in the middle.
- The rules are inputs: the drawdown in dollars, whether the floor trails the day's closing balance or every new high, whether it stops at the starting balance, the profit target, and whether a new account starts after one ends.
- The books: the account's balance is the strategy's equity since the account began, the open trade included, so one backtest can hold many accounts back to back.
- The day is CME's, from the 18:00 New York reopen: an end-of-day floor moves on the closing balance of the day before.
- Each candle is judged at the open trade's worst point inside it, as a firm's check on every tick would, and when the floor trails every new high, the trade's best point moves it after that check.
- When the floor is hit or the target reached, the trade closes at once, no new entry is taken that day, and the next account starts at the next session.
- The tally is in the Data Window and a table on the chart: accounts started, passed and failed.
//@version=6
// Suggested timeframe: 5-minute
strategy("NQ ORB with a prop firm trailing drawdown", overlay=true, initial_capital=50000, default_qty_type=strategy.fixed, default_qty_value=1,
commission_type=strategy.commission.cash_per_contract, commission_value=0.75, slippage=1, margin_long=0, margin_short=0)
// ── The trailing drawdown ─────────────────────────────────────────────────────────────────────────
// Example numbers, not any firm's: take yours from your plan's own page.
ddSize = input.float(2000, "Trailing drawdown ($)", minval=1, group="Trailing drawdown")
ddTrail = input.string("End of day", "The floor trails", options=["End of day", "Every new high"], group="Trailing drawdown")
ddLock = input.bool(true, "The floor stops at the starting balance", group="Trailing drawdown")
ddTarget = input.float(3000, "Profit target ($, 0 = none)", minval=0, group="Trailing drawdown")
ddRestart = input.bool(true, "After a pass or a fail, a new account starts the next day", group="Trailing drawdown")
// The account's books: dollars since it began, the open trade included
var float ddBase = strategy.initial_capital // the strategy's equity when this account began
var float ddPeak = 0.0 // the high the floor trails
var bool ddLive = true // this account is still trading
var int ddStarted = 1
var int ddPassed = 0
var int ddFailed = 0
// A CME day starts at the 18:00 New York reopen. An end-of-day floor moves on yesterday's close,
// and an account that ended yesterday makes way for a new one.
if session.isfirstbar
if ddLive
if ddTrail == "End of day"
ddPeak := math.max(ddPeak, strategy.equity - ddBase)
else if ddRestart
ddBase := strategy.equity
ddPeak := 0.0
ddLive := true
ddStarted += 1
ddPnl = strategy.equity - ddBase
// A firm checks every tick, not bar closes: the open trade's worst and best point inside this bar
ddPos = strategy.position_size
ddWorst = ddPnl - (ddPos > 0 ? (close - low) * ddPos : ddPos < 0 ? (high - close) * -ddPos : 0.0) * syminfo.pointvalue
ddBest = ddPnl + (ddPos > 0 ? (high - close) * ddPos : ddPos < 0 ? (close - low) * -ddPos : 0.0) * syminfo.pointvalue
ddFloor = ddLock ? math.min(ddPeak - ddSize, 0.0) : ddPeak - ddSize
if ddLive
if ddWorst <= ddFloor
strategy.close_all(comment="Drawdown hit", immediately=true)
ddLive := false
ddFailed += 1
else if ddTarget > 0 and ddPnl >= ddTarget
strategy.close_all(comment="Target reached", immediately=true)
ddLive := false
ddPassed += 1
// Trailing every new high: the open trade's best point moves the floor, after this bar's checks
if ddLive and ddTrail == "Every new high"
ddPeak := math.max(ddPeak, ddBest)
// ── The example strategy: the NQ opening range breakout from our study ───────────────────────────
// Your own entries go here, each behind ddLive.
orSess = input.session("0930-0945", "Opening range (New York)", group="Strategy")
tradeSess = input.session("0945-1200", "Entry window (New York)", group="Strategy")
riskUsd = input.float(500, "Risk per trade ($)", minval=10, group="Strategy")
rMult = input.float(2.0, "Target (R)", minval=0.5, step=0.5, group="Strategy")
maxQty = input.int(10, "Most contracts", minval=1, group="Strategy")
inOR = not na(time(timeframe.period, orSess, "America/New_York"))
inTrade = not na(time(timeframe.period, tradeSess, "America/New_York"))
cutoff = hour(time, "America/New_York") == 15 and minute(time, "America/New_York") >= 55
var float orHigh = na
var float orLow = na
var bool doneToday = false
var float stopLevel = na
var float targetLevel = na
if session.isfirstbar
orHigh := na
orLow := na
doneToday := false
if inOR
orHigh := na(orHigh) ? high : math.max(orHigh, high)
orLow := na(orLow) ? low : math.min(orLow, low)
rangeSet = not inOR and not na(orHigh)
longSetup = rangeSet and close > orHigh and close[1] <= orHigh
shortSetup = rangeSet and close < orLow and close[1] >= orLow
// one trade a day, sized from the risk: a stop too wide for one contract skips the day
canTrade = ddLive and inTrade and not doneToday and strategy.position_size == 0
if canTrade and longSetup and orLow < close
stopPts = close - orLow
qty = math.min(maxQty, math.floor(riskUsd / ((stopPts + 2 * syminfo.mintick) * syminfo.pointvalue)))
if qty >= 1
stopLevel := orLow
targetLevel := close + stopPts * rMult
strategy.entry("Long", strategy.long, qty=qty)
strategy.exit("Long exit", from_entry="Long", stop=stopLevel, limit=targetLevel)
doneToday := true
if canTrade and shortSetup and orHigh > close
stopPts = orHigh - close
qty = math.min(maxQty, math.floor(riskUsd / ((stopPts + 2 * syminfo.mintick) * syminfo.pointvalue)))
if qty >= 1
stopLevel := orHigh
targetLevel := close - stopPts * rMult
strategy.entry("Short", strategy.short, qty=qty)
strategy.exit("Short exit", from_entry="Short", stop=stopLevel, limit=targetLevel)
doneToday := true
if strategy.position_size > 0
strategy.exit("Long exit", from_entry="Long", stop=stopLevel, limit=targetLevel)
if strategy.position_size < 0
strategy.exit("Short exit", from_entry="Short", stop=stopLevel, limit=targetLevel)
// flat before the close, and on the session's last bar: on a short day CME closes early
if (cutoff or session.islastbar) and strategy.position_size != 0
strategy.close_all(comment="Cut-off", immediately=true)
plot(rangeSet ? orHigh : na, "Range high", color=color.new(#16A56A, 0), style=plot.style_linebr)
plot(rangeSet ? orLow : na, "Range low", color=color.new(#E5484D, 0), style=plot.style_linebr)
// the tally, in the Data Window and a table on the last bar
plot(ddStarted, "Accounts started", display=display.data_window)
plot(ddPassed, "Passed", display=display.data_window)
plot(ddFailed, "Failed on the drawdown", display=display.data_window)
plot(ddPnl, "This account's P&L", display=display.data_window)
plot(ddFloor, "Drawdown floor", display=display.data_window)
var table book = table.new(position.top_right, 2, 3, bgcolor=color.new(color.black, 15), border_width=1)
if barstate.islast
table.cell(book, 0, 0, "Accounts", text_color=color.white)
table.cell(book, 1, 0, str.tostring(ddStarted), text_color=color.white)
table.cell(book, 0, 1, "Passed", text_color=color.white)
table.cell(book, 1, 1, str.tostring(ddPassed), text_color=color.white)
table.cell(book, 0, 2, "Failed on the drawdown", text_color=color.white)
table.cell(book, 1, 2, str.tostring(ddFailed), text_color=color.white)

