PINE SCRIPT · PROP FIRM TRAILING DRAWDOWN

Trailing drawdown in Pine Script, kept the way a prop firm keeps it

Pine's built-in max_drawdown stopped our two-year MNQ test after 71 trades, $3,046 up. The code below keeps the drawdown the way a firm does, and ran the same trades as 22 accounts: 6 passed, 15 failed.

Run this script free

Opens on MNQ 5-minute with the backtest done. Free account, no card.

The same trades, one square per account
End of day (the code below)6 of 21 passed
Every new high8 of 26 passed
Every new high, never stops7 of 27 passed
Pine's built-in max_drawdown1 test, stopped

Example rules, not any firm's: $50,000, a $3,000 target and a $2,000 trailing drawdown that stops at the starting balance, except in the third row; two years of MNQ. Green passed, red hit the drawdown, grey was still running when the data ran out. The built-in's one square is its whole test: 71 trades.

How the floor moves

One account under the two ways a plan can trail, day by day. A $2,000 drawdown on $50,000, example numbers.

DayEnd of dayEvery new high
Start$50,000$48,000$48,000
Day 1A trade is up to $51,400, the day closes at $51,000$49,000, set at the close$49,400, set at the trade's high
Day 2A trade takes the account down to $49,300, the day closes at $50,200$49,000: still tradingHit at $49,300: the account has failed
Day 3The day closes at $52,600$50,000, not $50,600: it stops at the starting balance—

The same day passes under one rule and fails under the other: an intraday trail moves on open profit, so a trade that runs up and comes back lifts the floor into its own way. Read which one your plan uses, and whether its floor stops at the starting balance, then set the two switches at the top of the script.

The code

Tested on CME's MNQ data and passed by TradingView's compiler. Change the inputs to your plan's, and put your entries where the example's are.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. The tally is in the Data Window and a table on the chart: accounts started, passed and failed.
Pine Script v6 · 128 linesRun this script
//@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)
The drawdown on its own · 47 lines
// ── 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)

Paste it at the top of your strategy, under your strategy() line, and put ddLive and in front of each entry's condition.

What Run opens

The script above in the Alpha Charts terminal on MNQ 5-minute: its own screens, taken on 4 November 2026.

The terminal's chat with the trailing drawdown script read, put on MNQ1! and backtested, and its reply
It lands on MNQ 5-minute and runs. The Copilot reads the script, puts it on the chart and runs the backtest. Tell it your plan trails every new high, or give it your numbers: it edits the script and runs it again.
MNQ 5-minute on 20 October 2026 with the opening range drawn, one long trade and the script's table: 22 accounts, 6 passed, 15 failed on the drawdown
Every account on the chart. The script's own table counts them as it runs: 22 accounts, 6 passed, 15 failed on the drawdown. 20 October 2026: the 15-minute range drawn, one long, out at the 15:55 cut-off for +$119.
The terminal's results for the trailing drawdown script: +2.1% after costs over 475 trades, the last 6 months held back
The results, with the months it never saw. +2.1% after costs over 475 trades. The terminal holds back the last 6 months: +21.4% in the 18 months before them, −15.9% in them, and it says so.

Run your version free

The code is free to copy. Running it takes a free account, no card.

Free account

No card, no time limit.

$0
  • Unlimited backtests, costs charged, with the out-of-sample check
  • The Pine script audit and the full trade list
  • The Pine Copilot writes and edits your script
  • 50 AI credits a day

Paid plans add more AI, the optimiser and the rest of the desk, each with 30 days money back. See the plans

The same trades, kept other ways

Each run changes the drawdown's lines and nothing else: the entries, the data and the costs stay.

The drawdownAccountsPassedFailed
End of day, stops at the start: the code aboveThe script's defaults: the floor moves on the day's closing balance, stops at the starting balance, and is checked at each candle's worst point22615
Every new high, stops at the startThe floor moves at every new high, the open trade's best point included27818
Every new high, never stopsThe same, with no stop at the starting balance28720
End of day, checked at the candle's closeThe defaults, with the floor compared to the balance at each close instead of the candle's worst point226157 of the 15 failures came later: 6 the same day, one the next
The usual snippetThe high of the balance at each candle's close, never stopping, compared at the close27719
Pine's built-instrategy.risk.max_drawdown(2000, strategy.cash)1stopped 6 February 2025——71 trades, $3,046 up

How the floor trails decides more than its size. Trailing every new high, the open trade's best point lifts the floor, so accounts ended sooner: 27 in the same two years instead of 22, with 18 failures. Once the floor never stops at the starting balance, a profitable account keeps a floor under its own gains, and 20 of 28 failed.

The candle's close hides a fall. Checked at each close, the counts here came out the same, but 7 of the 15 failures were recorded later than they happened, one of them a day later. A firm ends the account at the tick.

The built-in rule tests one account, then stops. It knows no target and no floor that stops at the start, so it ran on past the account's first pass, on 4 December 2024, and stopped for good on 6 February 2025, $2,056 under its best closed balance. Every account after that went untested.

Questions

How do I code a trailing drawdown in Pine Script?

Keep the account's own books: its balance since it began, the open trade included, which is strategy.equity minus the equity when the account started. Trail the floor from the day's closing balance or from every new high, stop it at the starting balance if your plan does, and check it at each candle's worst point, not its close. When the floor is hit, close the trade and take no new entries. The script on this page does all of it in a block that goes on top of your own strategy.

What is the difference between an end-of-day and an intraday trailing drawdown?

An end-of-day drawdown moves its floor once a day, from the closing balance. An intraday one moves it at every new high, open profit included, so a trade that runs up and comes back raises the floor on the way. On the same trades over two years of MNQ, trailing every new high ended accounts sooner: 26 finished instead of 21, and 18 failed instead of 15.

Does strategy.risk.max_drawdown work as a prop firm trailing drawdown?

Not for testing a challenge. TradingView's manual says that once it is reached it cancels the orders, closes the position and stops all trading for the rest of the backtest, so two years of data answer for one account, and it has no profit target and no floor that stops at the starting balance. In our tester it counts the fall from the highest closed-trade balance: on the same trades it stopped on 6 February 2025 after 71 trades, $3,046 up and $2,056 under its best.

Should the check use the candle's close or its worst point?

Its worst point. A firm watches every tick, so a fall through the floor that recovers before the candle closes still ends the account. Checked at the close in this test, 7 of the 15 failures came later than they should have, one of them a day later.

Can I use my firm's numbers?

Yes. The drawdown, the target and the two switches are inputs. The script stores no firm's rules, and firms change them, so take the numbers from your plan's current page. Alpha Charts is not a prop firm and is not connected with any.

If my strategy stays inside the drawdown in a backtest, will it pass?

No one can say that. A backtest shows how past data would have treated these rules with these costs, and a challenge is judged on days that have not happened yet. Here the example entries made 2.1% over two years and still failed 15 of the 21 accounts that finished.

Do I need TradingView to run it?

No. It is Pine v6 and runs in TradingView, and Run this script opens it in the Alpha Charts terminal on MNQ 5-minute with the backtest done. A free account runs as many tests as you like, no card.

Test the whole challenge

The drawdown is one rule of a challenge. To test a strategy against all of them at once, the target, the daily loss limit, the drawdown and the cut-off, paste it into the prop firm challenge backtester. The daily limit on its own is on a daily loss limit in Pine Script, and which contract fits a 50K drawdown on MNQ or NQ for a 50K challenge.

Alpha Charts is software for writing and testing trading scripts. It is not a prop firm: it offers no challenges, evaluations or funded accounts, does not trade for you, holds no money and gives no investment advice. Backtests are past results with costs charged and do not predict passing a challenge or making money. The rules on this page are example numbers, not any firm's. MNQ and NQ are CME Group contracts.

The trailing drawdown scriptFree account, no cardRun it free