If you write TradingView strategies for futures prop firm accounts (Apex, Topstep, MyFundedFutures), there's a structural problem with your backtest: Pine Script's strategy() framework evaluates terminal equity, but prop firms evaluate a path-dependent constraint called trailing drawdown. A strategy can be profitable in the Strategy Tester and still fail evals consistently. Here's how to model the constraint correctly.
What trailing drawdown actually does
On most futures firms, your drawdown floor ratchets up with your account's high-water mark — and on many firms, that high-water mark includes unrealized profit. If you're up $600 open on a trade and it stops out at breakeven, your closed P&L is unchanged but your drawdown floor moved up $600 permanently. The backtest sees a scratch. The eval sees lost cushion.
Step 1: Track the unrealized high-water mark
//@version=6
strategy("Eval-Aware Backtest", overlay=true,
initial_capital=50000, commission_type=strategy.commission.cash_per_contract,
commission_value=0.67, slippage=1)
float liveEquity = strategy.equity
var float hwm = strategy.initial_capital
hwm := math.max(hwm, liveEquity)
strategy.equity in Pine already includes open profit, which is what we want for the high-water mark approximation. Run this on 1-minute bars for the closest approximation to tick-level ratcheting.
Step 2: Compute the trailing floor and breach condition
float trailDD = 2500.0 // 50k account, typical
float floorLevel = hwm - trailDD
float floorCapped = math.min(floorLevel, strategy.initial_capital + 100)
bool breached = liveEquity <= floorCapped
var bool evalFailed = false
if breached
evalFailed := true
strategy.close_all(comment="DD BREACH")
Step 3: Gate entries on eval state
bool profitTargetHit = strategy.equity >= strategy.initial_capital + 3000
bool canTrade = not evalFailed and not profitTargetHit
if canTrade and longSignal
strategy.entry("L", strategy.long)
Once evalFailed flips, the account is dead — letting the backtest keep trading after a breach is the single most common way Pine backtests overstate eval performance.
Step 4: Report what matters
var table t = table.new(position.top_right, 1, 3)
if barstate.islast
table.cell(t, 0, 0, evalFailed ? "EVAL FAILED" : profitTargetHit ? "EVAL PASSED" : "IN PROGRESS")
table.cell(t, 0, 1, "Floor: " + str.tostring(floorCapped, "#.00"))
table.cell(t, 0, 2, "Cushion: " + str.tostring(liveEquity - floorCapped, "#.00"))
Net profit is now a secondary metric. The number you optimize is whether the path ever touched the floor.
What changes when you do this
When I retrofitted this onto a library of strategies that all looked fine in the standard tester, about two-thirds failed simulated evals. The failures clustered: strategies with high MAE relative to target, and strategies with win rates under 40%.
The survivors — low MAE, 40-60% win rate, hard daily-loss gating — are a genuinely different population than "most profitable backtest." That's the core insight: the eval is a different objective function, and you have to encode it.
Run your own strategies through this harness before paying for another evaluation. If you'd rather start from strategies already built and validated against these exact constraints — with full source code so you can verify everything — that's what I sell at propfirmpinescripts.com, alongside free tools (drawdown calculator, eval simulator) that run this math in the browser.
Top comments (0)