Tutorial

Pine Script Equity Curve: Plot Strategy Equity and Drawdown

Plot strategy.equity in Pine Script v6, build a drawdown line from a running peak, and add a stats table, with code that compiles.

•10 min read

You run a backtest, the Strategy Tester says the net profit is positive, and then you open the equity tab and the line looks like a staircase falling down a hill with one lucky spike in the middle. The summary number and the shape of the curve are telling two different stories. That gap is why people want a Pine Script equity curve on their own chart: a line they can lay next to price, zoom into, and read bar by bar instead of trusting one total.

This post shows you how to plot strategy equity in Pine Script v6, how to turn it into a drawdown line, and how to put a few headline numbers in a table. Along the way we will cover what strategy.equity actually includes, why your plotted line can differ from the Strategy Tester's own chart, and what the shape of a curve can and cannot tell you.

What strategy.equity actually measures

In Pine Script, strategy.equity is a built-in series that holds the current value of your account in the simulation: the initial capital, plus the net profit from closed trades, plus the open profit of any trade that is still running. Because it includes open profit, it moves on every bar while you are in a position, not just when a trade closes.

That detail matters when you compare it with strategy.netprofit, which counts only closed trades. If you hold a long position that is 800 dollars in the green, strategy.equity reflects it and strategy.netprofit does not. When the trade closes at the end of the move the two lines meet again. Plotting both is a quick way to see how much of your result is still floating.

The related built-ins you will use most are strategy.initial_capital (the starting balance you set in the strategy() declaration), strategy.openprofit (the unrealised part), and strategy.max_drawdown (the largest peak to trough fall in equity so far, in account currency). The v6 reference lists all of them under the strategy namespace.

One more property worth knowing early: all of these are series values that the strategy engine updates as it processes each bar. You cannot assign to them, and you cannot read them in an indicator() script, because there is no simulated account there. The script must declare itself with strategy().

The simplest equity plot

Start with a small strategy so the curve has something to show. The example below is a plain moving average crossover with percent-of-equity sizing and a commission, and it plots equity and the starting capital in a separate pane.

pine
//@version=6
strategy("Equity Curve Basics", overlay=false, initial_capital=10000,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     commission_type=strategy.commission.percent, commission_value=0.05)

fastLen = input.int(10, "Fast length", minval=1)
slowLen = input.int(30, "Slow length", minval=2)

fast = ta.sma(close, fastLen)
slow = ta.sma(close, slowLen)

if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")

plot(strategy.equity, "Equity", color=color.blue, linewidth=2)
plot(strategy.initial_capital, "Starting capital", color=color.gray)

Two choices here deserve a comment. First, overlay=false puts the plots in their own pane. Equity is measured in account currency and price is measured in price, so drawing both on the same scale would crush one of them. Second, the commission is set on purpose. A curve with zero costs is flattering, and the shape changes noticeably once even a small percentage is charged on each side. If you want the full story on that, the post on slippage and commission walks through how much they take.

The grey starting capital line is not decoration. It gives you a baseline. When the blue line sits under it for a long stretch, you are looking at a strategy that spent most of the test underwater, whatever the final number says.

Why your curve may not match the Strategy Tester

Sooner or later you will plot strategy.equity, open the Strategy Tester's equity chart, and see two lines that do not quite agree. Usually the cause is one of three things.

The first is timing. Your plot is drawn on the bars of the chart you are viewing and moves with open profit on every bar, while the Strategy Tester's equity chart is its own view with its own sampling, so the two can look slightly different even when the underlying numbers agree.

The second is the order fill model. By default a strategy fills orders at the next bar's open, so a signal on the close of bar 100 changes equity from bar 101. If your plot is offset by a bar relative to what you expected from the signal, that is why. Read how TradingView backtesting actually works if you want the full picture of fills.

The third is intrabar behaviour. With calc_on_every_tick turned on, a strategy recalculates on each real-time tick, and the equity you see on a live bar is provisional until the bar closes. The post on calc_on_every_tick explains what that changes. For a clean historical curve, leave it off and read the closed bars.

Turning equity into a drawdown line

An equity curve answers "how much do I have". A drawdown line answers "how far below my best so far am I", and for most people that is the more honest question. A strategy that returns 30 percent over three years but sat 25 percent under its previous peak for eight months is a very different thing to live with than one that climbs steadily.

To compute drawdown you need the running peak of equity. In Pine Script that is a persistent variable that only ever moves up.

pine
//@version=6
strategy("Equity Drawdown", overlay=false, initial_capital=10000,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     commission_type=strategy.commission.percent, commission_value=0.05)

fast = ta.ema(close, 12)
slow = ta.ema(close, 26)

if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")

var float peakEquity = strategy.initial_capital
peakEquity := math.max(peakEquity, strategy.equity)

drawdownPct = (strategy.equity - peakEquity) / peakEquity * 100

plot(drawdownPct, "Drawdown %", color=color.red, style=plot.style_area)
hline(0, "Peak", color=color.gray)

The var keyword makes peakEquity initialise once and then keep its value from bar to bar; that is what lets it remember the highest equity seen so far. Without var the variable would reset on every bar and the drawdown would always be zero. If that mechanism is new to you, var, varip, and persistent state covers it properly.

The result is zero whenever equity is at a new high and negative whenever it is below one. Depth is how far down it goes, and the width of each red patch is how long recovery took. Both matter. A shallow but very long drawdown wears people out in a different way from a deep and short one, and a plain maximum drawdown figure cannot show you which you had. The post on maximum drawdown explains the number itself and how it can mislead.

You will also see why percent drawdown and strategy.max_drawdown differ. The built-in is in currency, measured peak to trough. Your plotted line is a percentage of the running peak. They are two views of the same fall.

A small statistics table on the chart

A curve is good for shape, but sometimes you want three numbers in the corner of the pane. A table does that, and drawing it only on the last bar keeps it cheap.

pine
//@version=6
strategy("Equity Stats Table", overlay=false, initial_capital=10000,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10)

fast = ta.sma(close, 20)
slow = ta.sma(close, 50)

if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")

plot(strategy.equity, "Equity", color=color.blue, linewidth=2)

var table stats = table.new(position.top_right, 2, 3, border_width=1)

if barstate.islast
    table.cell(stats, 0, 0, "Net profit", text_color=color.white, bgcolor=color.new(color.gray, 40))
    table.cell(stats, 1, 0, str.tostring(strategy.netprofit, "#.##"), text_color=color.white, bgcolor=color.new(color.gray, 40))
    table.cell(stats, 0, 1, "Max drawdown", text_color=color.white, bgcolor=color.new(color.gray, 40))
    table.cell(stats, 1, 1, str.tostring(strategy.max_drawdown, "#.##"), text_color=color.white, bgcolor=color.new(color.gray, 40))
    table.cell(stats, 0, 2, "Closed trades", text_color=color.white, bgcolor=color.new(color.gray, 40))
    table.cell(stats, 1, 2, str.tostring(strategy.closedtrades), text_color=color.white, bgcolor=color.new(color.gray, 40))

Creating the table with var means it is built once, and the barstate.islast check means the cells are written only on the final bar rather than thousands of times. If you want a larger dashboard, the post on Pine Script table objects goes much further.

Closed trade count is in the table for a reason. A smooth curve built from 12 trades is a very different statement from the same curve built from 400. Statistics from a small sample say very little, and the table makes that visible instead of hiding it in a tester tab.

How to read the curve without fooling yourself

Once the line is on your chart, the temptation is to read too much into it. A few habits keep the reading honest.

Look at the slope changes, not only the end. A curve that earns nearly all of its gain in one short window is leaning on a handful of trades, and removing them would leave a flat or falling line. If you want to know, you can check by scrolling through those bars and asking what the market was doing.

Compare the curve with a stretch of different conditions. Trending, ranging, and high volatility periods each treat a strategy differently. One long uptrend can lift a poor entry rule. The post on walk-forward testing shows how to split the data so you are not judging a rule only on the stretch you tuned it on.

Be suspicious of a curve that is too smooth. Real strategies have flat patches and ugly drops. A line that looks like a ruler often means a hidden problem such as a signal that uses future data. The lookback bias and repainting posts explain how that happens, and a curve that falls apart once you fix it was never telling you anything.

Finally, remember that an equity curve describes the past of one simulated run. It does not forecast anything. It is a debugging and understanding tool for the code and the rules, not a promise about what the next month will do.

Common mistakes when plotting equity

The most frequent error is plotting inside an indicator() script. There is no account to read, so strategy.equity simply is not available, and the compiler tells you so. Switch the declaration to strategy().

The second is plotting on the price pane. If you leave overlay=true, the equity line, which might be around 10,000 while price is around 60, forces the price scale to stretch and the candles flatten into a line. Use overlay=false.

The third is forgetting that var needs an initial value that makes sense. Starting peakEquity at strategy.initial_capital is correct because equity begins there. Starting it at zero would make every early bar look like a new peak.

The fourth is ignoring position sizing. If you size at a fixed quantity, the curve is linear in price moves. If you size as a percentage of equity, the curve compounds, and the same signals produce a visibly different shape. When you compare two versions of a strategy, change one thing at a time. The post on position size in Pine Script covers the options.

And the fifth is compile errors from small slips in the table or plot calls: a missing argument, a wrong namespace, an indentation that breaks a block. These are tedious to chase by hand. For a related walkthrough, see Pine Script Strategy Tester metrics, which explains what each number in the tester means so you know which ones are worth plotting.

Adding this to a script you already have

Most people do not start from an empty file. You have a strategy with 150 lines and you want an equity plot and a drawdown pane added without disturbing the entry logic. That is a small edit, and it is the kind of change where rewriting the whole file risks breaking something that worked.

In the PineScripter AI editor you can paste the script and ask in plain English for the equity line, the starting capital baseline, and the drawdown pane. The editor works against the Pine Script v6 manual, checks every script for compile errors the way TradingView does, and fixes the errors it finds on its own. A follow-up request changes only the affected lines, and the Code Changes view shows you the lines that moved, so you can confirm that your entry rules were not touched. When it is ready you copy the script into TradingView and read the curve in the Strategy Tester there. PineScripter itself does not run backtests or simulate trades, so the equity numbers always come from TradingView. You can see the plans on the pricing page.

Where to go from here

You now have three pieces: an equity line with a baseline, a drawdown line built from a running peak, and a small table with the totals. Start with the first, add the second when you want to judge risk, and keep the third small. The point of all of it is to make a backtest legible, so that when a number looks good you can see what is behind it.

If you want to understand which tester metrics deserve your attention, the expectancy and profit factor posts are a good next step, and the free max drawdown calculator lets you check your own peak to trough figures without any code.

Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.

Back to Blog

Frequently asked questions

What is strategy.equity in Pine Script?

It is a built-in series holding the simulated account value: initial capital plus net profit from closed trades plus the open profit of any running trade. It is only available in scripts declared with strategy().

How do I plot an equity curve in Pine Script?

Declare the script with strategy(), set overlay=false, and call plot(strategy.equity). Adding plot(strategy.initial_capital) gives you a baseline to compare against.

How do I calculate drawdown from the equity curve?

Keep a var variable that stores the highest equity seen so far with math.max, then compute (strategy.equity - peak) / peak * 100 and plot it as an area.

Why is my plotted equity different from the Strategy Tester chart?

The Strategy Tester has its own view and sampling, orders fill on the next bar by default, and calc_on_every_tick makes live bars provisional. Compare closed bars with calc_on_every_tick off.

Does PineScripter run backtests or draw the equity curve?

No. PineScripter does not simulate trades. It helps you write and edit the Pine Script, and you read the equity curve in TradingView.

Describe your indicator in plain English and get Pine Script that compiles →


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.