Educational Reference

Pine Script for Indian Traders: From an Idea to Something You Can Actually Test

Most people meet this language the same way. You have a rule in your head, you find a charting platform that lets you write it down, and within an hour you have something that draws arrows. The arrows look good. What almost nobody is told at that point is that a chart is allowed to show you things that were never knowable at the time, and that the default settings of a strategy tester are arranged in a way that flatters almost any rule. This page teaches the language properly, builds one strategy from an idea to a testable file, and then measures exactly how much three ordinary beginner errors were worth on a simulated series.

The finding, stated first. One breakout rule, one simulated ten year series, four ways of testing it. Written correctly and charged realistic costs the rule lost 15.0 percent. Allowed to act on the bar that had not yet closed, the same rule finished 220.9 percent up, and it did something similar on two hundred out of two hundred fresh simulated series. Nothing about the logic changed. Only the moment at which the script was permitted to decide. Illustrative and simulated throughout.

What this language is for, and what it is not for

Pine Script is the scripting language of a hosted charting platform. That single fact determines almost everything else about it, and it is worth being precise about the consequences rather than discovering them one at a time over several weeks.

What it does extremely well is turn a rule you can describe in words into something you can see on a chart and grade against history, in minutes rather than days. There is nothing to install, no data to source, no environment to configure, and no gap between writing a line and watching it draw. For the specific job of finding out whether an idea is even worth pursuing, that speed is not a convenience, it is the entire value. A rule that dies in twenty minutes has cost you twenty minutes.

What it is not is a place to run money. The platform simulates fills against its own bar data. It does not hold your account, it does not know your margin, it cannot see the order book you would actually be hitting, and its idea of a fill is an arithmetic assumption rather than a report from an exchange. Orders reach a real account only through a separate broker integration or by wiring alerts into some other piece of software, and at that point you have left the charting tool entirely and taken on order management, reconciliation, position state and a risk layer. Our guide to the four layers of algorithmic trading in India sets out which of those layers carry regulatory obligations, and it is worth reading before you assume the last mile is a small one.

There are three further limits that people tend to hit in roughly this order. The environment is closed, so there are no local files, no arbitrary libraries, and no version control that you control. The data is the platform's data, so a fundamentals panel, an alternative dataset or a survivorship free universe of delisted names is simply not available to you. And the whole mental model is one chart at a time, so anything that requires ranking a few hundred instruments against each other is awkward here and natural almost anywhere else.

None of that makes it the wrong tool. It makes it a specific tool. The question worth asking before you write anything is not whether the language is good, it is whether the shape of your question fits the shape of the environment.

Is this the right tool for what you are about to build? Four questions, asked in order. The first one that answers no is the one that decides. 1. Does the idea fit on a chart, one instrument at a time? yes, keep going no Ranking hundreds of names against each other is awkward here and natural elsewhere. 2. Are you testing a rule, not running money through it? yes, keep going no Order routing, reconciliation and a risk layer belong outside a charting platform. 3. Can you work inside a hosted, closed environment? yes, keep going no No local files, no arbitrary libraries, and no version control that you control. 4. Is the data you need already on the platform? yes, keep going no Fundamentals panels, alternative data and a survivorship free universe are not. Four times yes The fastest way to find out whether the idea deserves more work. Four times yes is not four times forever: the same rule can be rewritten somewhere else on the day one of the four answers changes.
Four questions in order. The first one that answers no is the one that decides. A yes to all four is a reason to prototype here, not a reason to stay.

The honest summary is that this is where a rule should be born and not where it should live. Prototype fast, find out quickly whether the idea has anything in it, and be entirely willing to rewrite the survivor somewhere else. Our walkthrough of vectorised backtesting of Indian equities in a general purpose language covers the environment you would move to and the problems that only become tractable there, particularly corporate actions and a universe that is not quietly composed of today's survivors. Neither page is a competitor to the other. They describe two different stages of the same process, and knowing which stage you are in is most of the skill.

The idea that causes most of the errors: your script runs once per bar

Here is the concept that separates people who find this language intuitive from people who find it maddening, and it has nothing to do with syntax.

Your file is not a program that starts, does some work, and stops. It is the body of a loop that the platform runs on your behalf. The engine takes the oldest bar on the chart, sets the built in variables to that bar's open, high, low, close and volume, runs your entire file from the first line to the last, stores whatever your lines produced, and then moves to the next bar and does the whole thing again. Two thousand bars means two thousand complete executions of every line you wrote.

Your script does not run once. It runs once per bar. Every line runs top to bottom on one bar, the result is stored, and then the whole file runs again on the next bar. run length ma signal plot bar 1 run length ma signal plot bar 2 run length ma signal plot bar 3 run length ma signal plot bar 4 run length ma signal plot bar 5 run length ma signal plot bar 6 run length ma signal plot bar 7 run length ma signal plot bar 8 closed and committed, one pass each still open time Historical bars One pass, using the bar's final open, high, low and close. The result is committed and never revised. This is what the tester grades. The bar still forming One pass per incoming tick. Before each pass the engine rolls back the previous one, so plots and conditions can appear, vanish and appear again.
The engine, not your file, owns the loop. Every line you write runs once for each bar on the chart. Historical bars are executed once with their final values and committed. The bar at the right hand edge is executed again on every incoming price update, with the previous execution rolled back first, which is where every repainting problem on this page begins.

Once you hold that picture, a lot of otherwise strange behaviour becomes obvious. There is no main function because there is nothing to start. There is no loop over the price history because you are already inside it. And every variable you declare is not one number but a whole column of numbers, one per bar, which the platform keeps for you.

That column is what people mean when they say every value in this language is a series. On the bar being processed right now, the name close refers to that bar's close. Write close[1] and you are reaching one row up the column, to the previous bar's close. Write close[20] and you are twenty rows up. The square brackets are not array indexing in the ordinary sense, they are a walk backwards through time, and the offset is always relative to whichever bar is currently being processed.

The practical consequence is that a great many things you would write as loops elsewhere are already done. You never iterate over history to compute an average; you call the average function and it returns a series whose value on each bar is the average as of that bar. You never store yesterday's value in a variable so you can compare it today; you write the offset and the engine has it.

The second consequence is subtler and it is where the real bugs come from. Because the whole file runs on every bar, a calculation that you put inside a condition only runs on the bars where the condition happened to be true. A function that maintains an internal record of past values, which includes almost every moving average, oscillator and range function in the standard library, can only build that record out of the bars on which it was actually called. Call it conditionally and its history develops holes, and the damage arrives as wrong numbers rather than as a failure to compile. The platform's own migration notes for the current language version are explicit about it: functions that depend on historical state have to be called at the top level, before any conditional block, if they are to calculate properly.

Listing 2. The same calculation, done wrongly and then correctly

//@version=6
indicator("Where beginners lose the history")

// WRONG. This line only runs on the bars where the condition happens to be
// true, so the average is built from a gappy, incomplete history.
float wrong = na
if close > open
    wrong := ta.sma(close, 20)

// RIGHT. Calculate on every bar, at the top level, then decide
// separately what to do with the answer.
float avg   = ta.sma(close, 20)
float right = close > open ? avg : na

plot(wrong, "Calculated inside a condition", color.red)
plot(right, "Calculated on every bar",       color.teal)

The rule that follows is simple enough to make a habit of. Calculate everything, unconditionally, at the top level of the file. Then decide separately what to do with the answers. In a strategy the only part that should ever be wrapped in a condition is the block that places orders, and that is exactly how the finished script later on this page is arranged.

There is one more piece of the model that matters enormously and that most introductions skip entirely. Historical bars and the bar currently forming are not treated the same way. A historical bar is executed exactly once, using its final values, and the result is committed and never revised. The bar at the right hand edge of the chart, the one still moving, is executed again on every incoming price update, and before each of those executions the engine rolls back the previous one. Plots are erased and redrawn. Conditions that were true become false. Nothing is committed until the bar closes. Every repainting problem later on this page grows from that one asymmetry.

What the language borrows from ordinary programming, and where it stops behaving like it.
What you would expectWhat happens insteadWhy it is built that way
A loop over the price historyThere is no loop to write. The platform runs your whole file once per bar, oldest bar firstThe chart is the iteration. Writing the loop yourself would duplicate what the engine already does
A variable holding one numberEvery variable holds one value per bar. Writing close[1] reaches the previous bar's value of that same variableIndicators are functions of history, so history is the default rather than something you have to store
A variable that survives the next iterationA plain assignment is rebuilt on every bar. Declaring it with var initialises it once and lets it persistMost calculations should forget. The ones that must remember are the exception and are marked as such
A main function that runs onceThere is no entry point. The file itself is the body that runs per barNothing needs to be started or stopped, so the smallest useful script is two lines
Calling a function only when you need itA function that depends on history has to be called on every bar or its internal record develops holesThe engine keeps that record for you, and it can only keep what it was allowed to see
Printing to a consoleplot, plotshape, bgcolor and labels, read off the chart and the data windowThere is no console. Output is visual because the medium is a chart
Importing any library, reading any fileOnly published libraries from the same platform, and only the platform's own dataThe environment is hosted and sandboxed, which is what makes it instant and also what limits it

A first indicator, complete and working

Start with a rule that can be said in one sentence: mark the bar on which price closes above the highest close of the previous twenty bars. That is a breakout, stated in a way that leaves no room for interpretation, which is the first discipline the language forces on you.

Listing 1. A complete first indicator

//@version=6
indicator("Breakout watch", overlay = true)

int lenInput = input.int(20, "Breakout lookback", minval = 2)

// ta.highest(close, n) includes the current bar, so the close would sit
// inside its own reference level. The [1] pushes the window back one bar
// and gives the highest close of the PREVIOUS lenInput bars.
float priorHigh = ta.highest(close, lenInput)[1]

bool broke = close > priorHigh

plot(priorHigh, "Prior high", color.orange, 2)
plotshape(broke, "Breakout", shape.triangleup, location.belowbar,
     color.teal, size = size.tiny)

Four things in that file are worth slowing down on, because each of them recurs in everything you will ever write.

The first line is not a comment. The version directive tells the compiler which dialect of the language to use, and the dialects differ in ways that will silently change your results if you copy a snippet from an older tutorial into a newer file. It has to be the first line, and it has to say which version you meant.

The input function is doing more than saving you a trip back into the code. Anything declared as an input becomes a field in the settings dialog, which means you can change it without editing, and more importantly it means the optimiser and the tester can change it too. A number hard coded into the body of a script is a number nobody will ever vary, which quietly guarantees that the one setting you happened to type first is the one you will end up trading.

The offset on the reference level is the part that separates a working indicator from a subtly broken one. The highest function, called with the current bar included, returns a level that the current bar's own close helped to set. Comparing the close to a level that contains the close is not a breakout test, it is a tautology dressed up as one. Pushing the window back one bar gives you the highest close of the twenty bars that had actually finished, which is the level a person watching the chart would have drawn.

Finally, the plotting calls do nothing except draw. That distinction sounds trivial and it is load bearing. If changing a plot changes what your rule does, the rule was reading something it should not have been reading, and the problem is upstream in the calculations rather than in the drawing.

Load that file on a daily chart and you have a working indicator. It is not a strategy, because it never says how much to buy, when to get out, or what it costs to be wrong. Those three questions are what the next section is about, and they are the difference between something that draws arrows and something that can be graded.

Turning an indicator into a strategy

A strategy script differs from an indicator in one structural way and a dozen consequential ones. Structurally, you swap the declaration, which unlocks a family of functions that place simulated orders and a results panel that grades them. Consequentially, you now have to answer every question the indicator was allowed to duck.

The five parts of a strategy script, and what belongs in each Order matters. Everything a later part needs must have been calculated on every bar, not only on the bars that trade. 1 Version and declaration //@version=6 and strategy(...) Costs, sizing and slippage are set here, not in the settings dialog, so the file carries them. 2 Inputs input.int, input.float, input.timeframe Anything you might want to change later. Hard-coded numbers are the enemy of testing. 3 Calculations Averages, ranges, filters, higher timeframe data Every one of these must run on every bar, at the top level, never inside an if block. 4 Orders strategy.entry, strategy.exit, strategy.close The only part allowed to be conditional. One decision per bar, taken on a confirmed close. 5 Visuals plot, plotshape, bgcolor Purely cosmetic. If a plot changes the result, something is wrong upstream. A calculation placed inside a condition does not exist on the bars where that condition was false.
The order of a strategy file is not a style preference. Calculations have to run on every bar so that the functions inside them keep an unbroken record. Only the order block should ever be conditional.

The rule we are going to build adds three things to the breakout. A trend filter on a higher timeframe, so the rule is not buying breakouts against a falling weekly picture. An exit that is not the same as the entry, because a rule with no exit is not a rule. And a protective stop derived from recent volatility rather than from a round number, so that the same code behaves sensibly on a quiet instrument and a violent one.

Before the whole file, the filter deserves its own paragraph, because requesting data from a higher timeframe is where the majority of published scripts go wrong, and where the next section of this page spends most of its time. There are three ways to ask the same question and only one of them gives you an answer you could have had at the time.

Listing 3. Higher timeframe data, requested three ways

//@version=6
indicator("Three ways to ask for the weekly close", overlay = true)

// 1. LEAKS THE FUTURE. On a historical chart this returns the week's
//    final close from the week's very first daily bar. Uncanny on
//    history, impossible in real time.
float leaky = request.security(syminfo.tickerid, "1W", close,
     lookahead = barmerge.lookahead_on)

// 2. DOES NOT LEAK, BUT MOVES. The default gives the last confirmed
//    value on historical bars and the developing value in real time,
//    so Wednesday's line is not the line you see there next month.
float drifting = request.security(syminfo.tickerid, "1W", close)

// 3. THE ONE TO USE. Offset the expression by one bar and turn
//    lookahead on. The two go together: the offset removes the future,
//    and lookahead makes the historical alignment match the live one.
float settled = request.security(syminfo.tickerid, "1W", close[1],
     lookahead = barmerge.lookahead_on)

plot(leaky,    "Leaks the future",       color.red,  2)
plot(drifting, "Moves during the week",  color.gray, 2)
plot(settled,  "Last confirmed weekly",  color.teal, 2)

The version to use is the third. Offset the expression by one bar inside the request, and turn lookahead on. Those two changes are a pair and neither of them works alone. The offset removes the part of the higher timeframe bar that had not happened yet. Turning lookahead on then makes the historical alignment behave the same way the live chart behaves, so that the value you see on a two year old bar is the value the script would genuinely have had on that day. The platform's own documentation is unusually blunt about this, noting that the offset and the lookahead setting are interdependent and that removing either one compromises the result.

With the filter written safely, the whole strategy fits on one screen.

Listing 5. The strategy, complete

//@version=6
strategy("Weekly filtered breakout, illustrative only",
     overlay                 = true,
     initial_capital         = 100000,
     default_qty_type        = strategy.percent_of_equity,
     default_qty_value       = 20,
     commission_type         = strategy.commission.percent,
     commission_value        = 0.03,
     slippage                = 2,
     margin_long             = 100,
     process_orders_on_close = false,
     calc_on_every_tick      = false)

// ---------- inputs -------------------------------------------------------
entryLen = input.int(20, "Entry lookback", minval = 2, group = "Rule")
exitLen  = input.int(10, "Exit lookback",  minval = 2, group = "Rule")
maLen    = input.int(10, "Filter average", minval = 2, group = "Rule")
atrLen   = input.int(14, "ATR length",     minval = 1, group = "Risk")
htfInput = input.timeframe("1W", "Filter timeframe",   group = "Rule")
atrMult  = input.float(3.0, "Stop distance in ATR", minval = 0.5,
     step = 0.5, group = "Risk")

// ---------- calculations: every bar, top level, never conditional --------
float priorHigh = ta.highest(close, entryLen)[1]
float priorLow  = ta.lowest(close,  exitLen)[1]
float atrValue  = ta.atr(atrLen)

bool htfUpRaw = close > ta.sma(close, maLen)
bool htfUp    = request.security(syminfo.tickerid, htfInput, htfUpRaw[1],
     lookahead = barmerge.lookahead_on)

// Both operands are already computed above, so the lazy "and" of version 6
// cannot skip a calculation that later bars depend on.
bool goLong    = htfUp and close > priorHigh
bool standDown = close < priorLow

// ---------- orders: the only conditional part ----------------------------
if strategy.position_size == 0 and goLong
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0
    strategy.exit("Protective stop", from_entry = "Long",
         stop = math.round_to_mintick(
              strategy.position_avg_price - atrMult * atrValue))
    if standDown
        strategy.close("Long", comment = "exit band")

// ---------- visuals: cosmetic only ---------------------------------------
plot(priorHigh, "Entry level", color.new(color.orange, 30))
plot(priorLow,  "Exit level",  color.new(color.gray,   30))

Read the declaration first, because it is doing more work than any other part of the file. Costs, sizing, slippage and the tick by tick calculation setting all live there. Putting them in the code rather than leaving them in the settings dialog means the assumptions travel with the file, so anyone who opens it sees what it was tested under. A script that specifies nothing is a script tested at zero cost with one share per trade, and neither of those was a decision anybody made on purpose.

Then read the order block, which is short by design. There is exactly one place where an entry can happen and exactly one place where an exit can happen, both guarded by the current position state, so the file can never place an entry it did not intend or leave a position without a stop. The protective stop is re-issued on every bar while the position is open, which is how you keep an exit order alive and current rather than placing it once and hoping. That arrangement has a visible seam worth naming rather than glossing over: the position does not exist until the entry fills on the bar after the signal, so the first bar of every trade is held without a stop. The honest options are to accept a gap of one bar, or to place the exit in the same block as the entry and derive its level from the signal bar's close instead of from the fill price. Either is defensible. Not noticing is not.

The sizing in that version is a blunt instrument: a fixed fraction of equity per trade, set in the declaration. It is a reasonable default and it is not what a risk aware version would do, because a fixed fraction of equity takes the same rupee position whether the stop is two percent away or nine percent away. Sizing from the stop distance instead makes every trade risk about the same amount.

Listing 6. Sizing the position from the stop, not from the price

// Replaces the entry block in Listing 5. Everything else is unchanged.

riskPct = input.float(0.75, "Risk per trade, percent of equity",
     minval = 0.05, maxval = 5.0, step = 0.05, group = "Risk") / 100.0

float stopPrice = math.round_to_mintick(close - atrMult * atrValue)
float riskUnit  = close - stopPrice
float qty = riskUnit > 0 ? strategy.equity * riskPct / riskUnit : 0.0

if strategy.position_size == 0 and goLong and qty > 0
    strategy.entry("Long", strategy.long, qty = qty)

One honest note about that block. The quantity is computed from the current close, but the order fills on the next bar, so the risk actually taken will differ slightly from the risk intended whenever the market opens away from the previous close. That is not a bug you can code around inside a bar based tester, it is a real property of trading on closes, and the right response is to know the size of it rather than to pretend it is zero. Our page on the machinery that surrounds a strategy deals with position sizing and portfolio heat at the level of the whole account, which is the level at which this stops being a rounding detail.

Repainting: the section most tutorials get wrong

Repainting is the single most valuable thing to understand about this language, and it is also the thing most commonly explained badly. The usual treatment reduces it to a warning about one function argument. It is broader than that, and the breadth is the point: repainting is not one bug, it is a family of bugs that all share the same shape.

Here is the shape. Somewhere in your script, information that belonged to a later moment has been allowed to reach a decision that appears to have been taken earlier. That is all of it. Every specific cause is a different route by which the future gets into the past, and every symptom is the same: a historical chart that looks better than anything you can reproduce live, with no error message and nothing obviously wrong in the code.

Cause one: asking a higher timeframe for something it has not finished saying

This is the most damaging cause because it is the least visible. Suppose your daily strategy consults a weekly trend filter. A week has five daily bars. The weekly bar's close is not known until Friday. But when the platform lines up weekly data against daily bars on a historical chart, it has to decide what value to hand you on Monday, and the setting that controls that decision defaults to something reasonable and can be switched to something catastrophic.

Turn lookahead on without offsetting the expression and the answer you get on Monday is the value the week ended up with on Friday. Every Monday, every Tuesday, every Wednesday of every week in your entire history, your script knew how the week would finish. On history this is invisible, because the line simply looks like a very well behaved weekly value. Live, the same code cannot possibly do it, because Friday has not happened.

What the chart shows you afterwards is not what you could have known The same four weeks of daily bars, and the same weekly filter, drawn two ways. Green means the filter says the weekly trend is up. Drawn with lookahead on and no offset every day of the week is coloured by that week's own closing verdict, which did not exist until Friday Drawn from the last week that actually closed the colour steps only at a week boundary, so Monday knows exactly what Monday could know days on which the two panels disagree 10 of 20 week 1 week 2 week 3 week 4 On the ten year simulated series behind this page the two versions of this one filter disagreed on 16.2 percent of all bars.
The same four weeks of daily bars, with the same weekly filter drawn two ways. Above, the filter is requested with lookahead on and no offset, so every day of the week is coloured by a verdict that only existed once the week had closed. Below, the same filter restricted to the last week that actually finished. Ten of the twenty days disagree. Illustrative simulated series.

The default setting is safer but it is not clean either, and this is the part that most explanations miss. With lookahead left off, historical bars receive the last confirmed higher timeframe value, which is correct, but the bar still forming in real time receives the developing value, which changes as the week progresses. So the line you watch on Wednesday afternoon is not the line that will be there when you scroll back to that Wednesday next month. There is no future leak, but the chart still disagrees with its own history, and a strategy built on it will still behave differently in real time than the tester promised.

The version that is clean in both directions is the third one: offset the expression by one bar and turn lookahead on. That combination returns the last higher timeframe bar that genuinely finished, identically on history and in real time. It is one extra character and a named argument, and it is the difference between a filter and a fiction. One warning that belongs here because the documentation states it and almost nobody repeats it: the built in flag for a confirmed bar does not work inside a higher timeframe request, so you cannot fix a leaky request by adding a confirmation check to the expression.

Cause two: acting on a bar that has not finished

The second route is the one that ruins live trading rather than backtests, and it comes straight out of the execution model. While a bar is open, your script runs again on every price update, and the engine discards the previous run before each new one. A condition can therefore become true at eleven in the morning, draw an arrow, fire an alert, and then become false again by the close, leaving nothing behind. On the historical chart, where each bar was executed once with its final values, that arrow never existed at all.

The fix is to require the bar to be finished before the condition counts. The relevant built in flag is true on every historical bar and true on the closing update of a live bar, which is exactly the behaviour you want: identical treatment of history and of the present.

Listing 4. Acting only on a bar that has finished

//@version=6
indicator("Confirmed only", overlay = true)

float priorHigh = ta.highest(close, 20)[1]

bool raw       = close > priorHigh
bool confirmed = raw and barstate.isconfirmed

// raw flickers on and off while the bar is still moving.
// confirmed can only become true once, on the bar's final update.
plotshape(raw,       "Raw",       shape.circle,     location.abovebar,
     color.new(color.gray, 40), size = size.tiny)
plotshape(confirmed, "Confirmed", shape.triangleup, location.belowbar,
     color.teal, size = size.tiny)

alertcondition(confirmed, "Breakout confirmed",
     "Close above the prior high")

The same discipline applies to alerts. An alert configured to fire on every tick will fire on conditions that later evaporate. Setting it to fire once per bar close, and making the underlying condition require a confirmed bar as well, means the alert you receive corresponds to something that is still there when you look.

Cause three: the strategy setting that quietly rewrites your history

A strategy declaration accepts a setting that makes the script calculate on every tick rather than once per bar. It sounds like a fidelity improvement. It is the opposite. Historical bars have no ticks stored, so the past is still calculated once per bar, while the present is calculated continuously. The result is a strategy whose behaviour changes at the boundary between history and now, which means the number in the tester was produced by one set of rules and your live trading will be produced by another. It also means nobody else can reproduce your result, because the answer depends on when the chart happened to be loaded.

Two related constructs belong in the same paragraph. Variables declared to persist across ticks cannot behave consistently, because historical bars have no ticks for them to accumulate across. And the built in that reports whether the script is currently on a live bar, by definition, gives a different answer on history than on the present, so any signal that consults it is guaranteed to be inconsistent. All three are legitimate tools for building a live dashboard. None of them belongs anywhere near the code that generates an order.

Cause four: reading a bar's extremes

The quietest cause of all. A rule that references the current bar's high or low is reading, on every historical bar, a number that was only knowable once the bar had ended. Testing a rule such as buying when price touched the low of the bar and closed above the midpoint will produce an excellent backtest and an impossible live experience, because on the live bar the low is provisional until the bar closes. The same applies to any rule that compares the current close to a range that includes the current bar.

Every common cause of repainting, the symptom on your chart, and the change that removes it.
CauseWhat you noticeWhat to write instead
Higher timeframe request with lookahead on and no offsetHistorical signals land at the exact turn of every higher timeframe bar, with a precision that never repeats liveOffset the expression by one bar and keep lookahead on. The pair is the fix, not either half
Higher timeframe request left on the defaultNo future leak on history, but the line moves all week and then settles, so the chart disagrees with itselfThe same fix. Ask for the last bar that finished, not the one still forming
A condition evaluated on a bar that is still openArrows and highlights appear, vanish and reappear as price moves inside the barGate the condition with barstate.isconfirmed, or read the previous bar with [1]
calc_on_every_tick = true in a strategy declarationThe tester result changes depending on when you opened the chart, and nobody else can reproduce itLeave it false. Turn it on only for a specific realtime behaviour you can justify
Variables declared with varipHistory and live can never agree, because the historical bars have no ticks to accumulateUse var. Reserve varip for tick counters that are never part of a signal
timenow, barstate.isnew or barstate.isrealtime in the signal pathThe strategy tester shows one thing and the live chart shows another, permanentlyKeep them out of anything that produces an order. They are display and diagnostic tools
An alert set to fire on every tickThe alert arrives, you look at the chart, and the condition that triggered it is no longer thereFire once per bar close, and make the underlying condition require a confirmed bar as well
A rule that reads the current bar's high or lowOn history the extreme of the bar is a known number. Live, it is not known until the bar endsReference high[1] and low[1], or build the rule on the close

How to check your own script

Three checks catch nearly everything, and none of them requires a testing framework.

Compare the two versions side by side. Plot the leaky form and the clean form of the same higher timeframe value on one chart and look at where they step. The leaky one changes at the start of every higher timeframe bar to a value that bar had not reached yet, which on a historical chart looks eerily prescient.

Listing 7. A two line test you can run on any chart

//@version=6
indicator("Does this value change after the fact?", overlay = true)

float leaky   = request.security(syminfo.tickerid, "1W", close,
     lookahead = barmerge.lookahead_on)
float settled = request.security(syminfo.tickerid, "1W", close[1],
     lookahead = barmerge.lookahead_on)

plot(leaky,   "Leaky",   color.red,  2)
plot(settled, "Settled", color.teal, 2)

Use bar replay. Wind the chart back and step forward one bar at a time. If a signal that is present on the finished chart fails to appear at the moment it should have, or appears and then disappears as the next bar forms, you have found a repaint. This is the single most useful habit on this page and it takes about four minutes.

Screenshot the right hand edge and come back. Save an image of the last twenty bars with your signals on them, wait a few days, and compare. Anything that moved, moved after the fact. This one is slow and it is also the only check that catches every cause at once, because it compares what you actually saw with what the chart later claims you saw.

What each mistake was actually worth

Warnings are easy to write and easy to skim. The rest of this section is arithmetic instead, so the size of each problem is a measured quantity rather than an adjective.

The setup is deliberately plain. One synthetic daily series of 2,500 bars, about ten years, generated from ten stated regimes with defined drift and volatility so that the history contains advances, chop and two drawdowns rather than one long rise. One rule, the breakout described above with its weekly filter. One cost assumption, six basis points a side, so twelve basis points a round trip. And then the same rule tested four ways, changing nothing about the logic and only changing when the script was permitted to act and what it was charged.

One rule, four ways of testing it. Only one of them was tradeable. Identical entry and exit logic on one identical simulated series. The only differences are when the script may act and what it is charged. Log scale. 0.8x 1x 1.25x 1.6x 2x 2.5x 3.2x about ten years of simulated daily bars starting equity = 1.0x acting on the bar that has not closed +220.9% higher timeframe lookahead +26.1% no costs charged −9.7% every rule obeyed, costs charged −15.0% The gap is the whole lesson Same logic, same data, same instrument. Three settings decided the answer.
One rule, one simulated series, four testing conventions. Log scale, starting equity 1.0x. The green line is the only version that could have been traded. The gap between it and the coral line is one bar of timing, nothing else. Illustrative and simulated, not a track record.

The rule written correctly and charged realistic costs lost 15.0 percent of its starting equity over the full period. It was in the market about thirty percent of the time and took fifty entries. That is the honest answer, and the honest answer is a loss. Buying and holding the same series lost 3.4 percent, so on this particular draw the rule did worse than doing nothing while still generating fifty round trips of cost and effort.

Allowed to consult the weekly filter with lookahead, the same rule finished 26.1 percent up. The only difference is that on any given day inside a week it knew what that week's close would be. On this series the two versions of that one filter disagreed on 16.2 percent of all bars, and that disagreement was enough to change the position actually held on 4.7 percent of them. A small leak, applied consistently, moved the ten year total by 41 percentage points.

Allowed to act on the bar that had not yet closed, the same rule finished 220.9 percent up. This is the largest distortion by a wide margin, and the reason is worth stating precisely rather than hand waving. The entry condition compares the current close to a level set by earlier bars. If the position is credited with the return of the bar that triggered the signal, then every entry captures the up move that caused the breakout and every exit dodges the down move that caused the exit. Fifty round trips of that, compounded, is what a 236 percentage point swing looks like.

Charged nothing at all, the honest version lost 9.7 percent instead of 15.0 percent, so removing costs was worth about 5 percentage points. That is the smallest of the three distortions on this series, which is worth saying out loud because cost is the one people most often treat as the big lie. At twelve basis points a round trip and fifty round trips, it is real and it is not the main event. It becomes the main event at higher turnover, which is the case for anything intraday.

Combine all three and the same rule shows 429.0 percent, against a truthful loss of 15.0 percent. Nothing about the logic changed at any point.

One result, or a property of the method?

A single simulated series proves very little, and a page that argues against reading noise should not then ask you to read noise. So the entire comparison was re-run on two hundred independently generated series with the same regime structure and a fresh random draw each time.

Repeated on 200 independent simulated series, not one lucky draw Percentage points each mistake added to the ten year total, against the same rule tested correctly on the same series. +0 +100 +200 +300 +400 Acting on the bar that has not closed added in 100 percent of the 200 runs median +276 Higher timeframe lookahead added in 91.5 percent of the 200 runs median +24 No costs charged added in 100 percent of the 200 runs median +7 percentage points added to the ten year total shaded bar spans the middle half of the 200 runs one rule, 200 fresh synthetic series
The whole comparison repeated on two hundred independently generated series. The shaded bar spans the middle half of the runs and the line is the median. Acting on a bar that has not closed inflated the result in all two hundred. Illustrative and simulated.

Acting on the unconfirmed bar added a median of 276 percentage points, with the middle half of the runs between 207 and 363, and it added something in every single one of the two hundred. The higher timeframe leak added a median of 24 percentage points and helped in 91.5 percent of runs. Charging nothing added a median of 7 percentage points and helped in every run, as it must, since removing a cost cannot lower a result. The honest version itself had a median ten year total of 13.7 percent across the two hundred series, so the one illustrated above was a below average draw rather than a rigged one.

The inconvenient part of this exercise is worth publishing rather than hiding. The size of the unconfirmed bar distortion is not a constant. It depends entirely on how tightly the signal keys off the current bar's close. The same test run on a slow moving average crossover, where the state changes only gradually and one day's close barely moves the average, produced a gap of about ten percentage points rather than the two hundred and thirty six above. So the honest statement is not that acting on an unfinished bar always inflates a result enormously. It is that it always inflates a result, and the inflation is largest for exactly the kind of rule most people write first, which is a rule that triggers on the current close crossing a level.

Costs, and why the defaults flatter everything

Every strategy tester ships with a set of defaults, and every set of defaults has a direction. Understanding which way each one leans is more useful than memorising a recommended value, because the right value depends on what you trade and the direction of the error does not.

Commission defaults to zero. Slippage defaults to zero. Order size defaults to a single unit. Initial capital defaults to a round number that is probably not yours. Margin now defaults to full funding, which is the conservative choice, but it was not always so and older scripts carry the older assumption. Every one of those defaults, except the last, makes a strategy look better than it is, and the reason is not conspiracy. A tester cannot know your costs, so it charges none, and the burden of supplying them falls on the person who is least motivated to be pessimistic.

The strategy tester settings that change the answer, their defaults, and a defensible starting point. Illustrative guidance, not a recommendation.
SettingWhat it defaults toA defensible starting point and why
initial_capital1,000,000Your real account size. Percentage of equity sizing is meaningless if the equity is fictional
default_qty_typeOne contract or share per tradePercentage of equity, or better, a size derived from the distance to your stop. One share makes every summary figure unreadable
commission_type and commission_valuePercent, and zeroA percentage that covers your whole charge stack, not just the brokerage line. Zero is the fastest way to manufacture an edge
slippageZero ticksAt least one or two ticks on something liquid, more on something thin. Market and stop orders do not fill at the last printed price
pyramidingOne entry in the same directionLeave it at one unless the rule genuinely adds to a position. Anything higher hides sizing mistakes behind a better looking curve
process_orders_on_closeFalseLeave it false. Setting it true fills market orders at the close of the signal bar, which is a claim about your execution that needs justifying
calc_on_every_tickFalseLeave it false. True makes the historical result depend on when the chart happened to be loaded
margin_long and margin_short100Keep them at 100 for a cash instrument. Lowering them silently adds leverage to every number the tester reports
use_bar_magnifierFalseTurn it on where your plan allows. It orders intrabar fills from real lower timeframe bars instead of from an assumption about how price moved inside each bar

Two of those rows deserve a longer note.

The commission field is a percentage, which invites you to type the brokerage rate and move on. That is the wrong number. What matters is the total cost of a round trip, which on Indian equities and derivatives includes exchange transaction charges, statutory levies, the goods and services tax on the chargeable components, and the stamp duty on the buy side. Our page on the backtesting procedure works through that stack line by line with verified rates, and the arithmetic there is the input this field wants. Typing a plausible looking small number instead is how a strategy passes a test it should have failed.

Slippage is measured in ticks and it models something different from commission: the gap between the price your rule saw and the price your order actually got. A market order placed at a breakout is by definition arriving when the book is thin on the side you want. A stop order is worse, because it converts to a market order at the least convenient moment. The number to use is not a universal constant, it is a property of your instrument, your size and your time of day, and the useful discipline is not finding the perfect value but checking how much of your result survives when you double whatever value you chose.

There is a third setting that is not really a cost but behaves like one. A bar based tester holds four prices per bar, so when one bar's range contains both your stop level and your target level it has to assume an order in which price visited them. The documented assumption is mechanical: if the open sits nearer the high than the low, the emulator treats the bar as having moved open, high, low, close, and otherwise as open, low, high, close. That assumption is sometimes right and sometimes exactly backwards, and it is applied to every ambiguous bar in your history without comment. The bar magnifier setting replaces the assumption with actual lower timeframe bars, so the ordering comes from data rather than from a rule of thumb, and any result that changes materially when you switch it on was resting on the rule of thumb.

What the strategy tester does not model

Suppose you have done all of the above. The script is clean, the filter is offset correctly, orders are placed on confirmed bars, costs are honest and the magnifier is on. The tester now shows a result. What is that result entitled to claim?

Less than it appears. Here is the list of things it did not model, stated plainly, because knowing the boundary is what turns a number into evidence rather than a conclusion.

It did not model the queue. A fill in the tester is an arithmetic event. In a real book your order joins a queue, and whether it fills depends on how much size is ahead of you, which the tester has no way of knowing. For a small position in a liquid instrument this is a minor abstraction. For a large position, an illiquid instrument, or an option strike nobody is quoting, it is the whole story.

It did not model your own behaviour. The tester follows the rule perfectly through a run of losses that would have made you question the rule. Every backtest is implicitly a test of a person who does not exist.

It did not model survivorship or corporate actions properly. Whatever adjusted series the platform hands you encodes decisions about splits, bonuses and dividends that you did not make and mostly cannot inspect, and the instrument list you can pick from is a list of things that still exist. Testing a rule on an instrument that is present today tells you nothing about the ones that are not.

It did not do a walk forward. This one is worth naming specifically because it is a genuine structural limitation rather than a data problem. The tester grades a single set of parameters over a single span, and its optimiser grades many sets over that same span and hands you the best one, which is the most contaminated result in the table. Doing it properly means choosing settings on one block of history, applying them unchanged to the next block, keeping only the blind result, and rolling forward. Our worked walk forward analysis runs exactly that procedure and publishes every window, including the ones that undercut the tidy story. The mechanical parts of it, the rolling and the record keeping, are not something a chart based tester will do for you.

It did not stop you testing fifty ideas and keeping the best. No tool can. That is overfitting one level up, and the only defence is deciding your rule and your rejection criterion before you look. Our catalogue of the eight ways a backtest can lie is the pillar page for that whole family of problems, and repainting is only one entry on it.

What a clean tester result is actually entitled to claim is narrow and still useful: on this instrument, over this history, under these cost assumptions, a rule stated in advance was not obviously broken. That is a reason to move on to a harder test. It is not a reason to fund an account. The pipeline from that point onward, through paper trading, forward testing and the compliance gate that sits in front of any automated order flow in India, is set out in our guide to starting algorithmic trading in India.

Where this fits in a working process

The most common way this language gets used badly is not technical. It is that people mistake fluency for progress. Writing a script that compiles and draws is a genuine skill and it takes about a week. Writing a script whose result you would defend to somebody hostile takes considerably longer, and almost all of the extra time goes into the parts nobody demonstrates on video: the offset on the higher timeframe request, the confirmation check before the order, the cost figure that came from an actual arithmetic exercise rather than from a plausible looking guess.

The most common way it gets used well is unglamorous. You have an idea. You write the smallest version of it that can produce a number. You check the three repaint routes deliberately rather than hoping. You charge realistic costs. You look at the result, and most of the time you delete the file, because most ideas do not survive being written down precisely. The value of the language is not the strategies it produces. It is the speed at which it disposes of the ones that were never going to work, and that speed only exists if the testing is honest enough to say no.

The habits are portable and the language is not. Every discipline on this page, calculating unconditionally, acting only on finished information, charging what things cost, and deciding rejection criteria before looking, applies identically in any environment you might move to later. That transferability is the argument for learning them here, where the feedback loop is measured in seconds. If the arithmetic on this page was the interesting part rather than the tedious part, that is the method we teach.

FAQ

Frequently asked questions

It is the scripting language of one hosted charting platform, and it is built for two jobs: drawing something on a chart that is not already there, and asking what a rule would have done on the history behind that chart. It is very good at both. It is not an execution stack, it does not run on your machine, it cannot read a file, and it has no package ecosystem. Treating it as a research and prototyping tool rather than as a trading system is the distinction that keeps people out of trouble.

No, and that is part of why the language exists. The syntax is small and the platform handles everything around it. What you do need is a willingness to be precise, because the language will faithfully implement whatever you actually wrote rather than what you meant. Most of the difficulty people hit is not syntax at all, it is the execution model, which is unusual and which no amount of prior programming experience prepares you for.

Your file is not a program that starts, does something and finishes. It is the body of a loop that the platform runs for you, once for every bar on the chart, from the oldest to the newest. Every variable therefore holds a whole column of values, one per bar, and the square bracket operator reaches back into that column. Once you hold that picture, most of the language stops being surprising.

Repainting is when the chart in front of you shows something that could not have been seen at the time it appears to have happened. It has several causes, but they all share one shape: information from later in a bar, or from later in a higher timeframe bar, has been allowed to reach a decision that was supposedly made earlier. The result looks superb on history and behaves quite differently in real time, and the discrepancy is never announced.

Offset the expression by one bar inside the request and turn lookahead on. Those two changes are a pair and neither works without the other. The offset removes the part of the higher timeframe bar that had not happened yet, and lookahead makes the historical alignment match what a live chart would show. Written that way, the value you get is the last higher timeframe bar that actually finished, which is exactly what you would have known.

Almost always because the alert is firing on a bar that has not finished. While a bar is open the script re-runs on every incoming tick, so a condition can become true, fire, and then become false again before the bar closes, leaving no trace on the chart. Gating the condition on the bar being confirmed, and setting the alert to fire once per bar close, removes the mismatch.

Something rather than nothing is the first and largest improvement, because both fields default to zero and a zero cost test is not a test. The defensible approach is to work out your own all-in cost for one round trip on the instrument and timeframe you are actually testing, express the broker charges as a percentage and the execution shortfall as a number of ticks, and then check how much of the result survives when you double both.

Not by itself. The platform simulates fills; it does not have your money. Orders reach a live account only through a connected broker integration or by wiring alerts into something else that does the placing, and the moment you do that you have left the charting tool and entered the territory of order management, reconciliation and risk limits, which is a much larger engineering problem than the strategy was.

When the question you want to ask stops fitting on a chart. Ranking a few hundred instruments against each other, joining prices to some other dataset, running thousands of parameter combinations, or reproducing a result outside a hosted environment are all things a general purpose language does comfortably and a charting language does badly. Prototype where it is fast, move when the constraint starts to shape the question.

No. A good result means the rule was not obviously broken on that history, on that instrument, under those assumptions. It says nothing about conditions absent from the data, nothing about whether your fill assumptions hold at your size, and nothing about whether you would actually have followed the rule. It is a reason to keep going, and specifically a reason to move on to harder tests, not a reason to fund an account.

Method note

How the numbers on this page were produced

Every figure comes from a single deterministic simulation, seeded so that it reproduces identically on each run. The price series is synthetic, built from ten stated regimes with defined drift and volatility, and it is not a model of any specific security or index. The rule tested is a long only breakout: enter when the close exceeds the highest close of the previous twenty bars while a weekly trend filter is positive, exit when the close falls below the lowest close of the previous ten bars. Returns are computed on closes. In the honest version the position on a bar comes from the signal produced at the close of the bar before it, and a cost of six basis points is charged on each side of every position change. The three distorted versions change one thing each and nothing else. The whole comparison is then repeated on two hundred independently generated series, and the medians and quartiles reported here come from that sweep.

The protective stop and the risk based sizing shown in the code listings are not part of the simulated comparison, which measures signal timing and cost assumptions only. Every code listing on this page was written against the current official language reference and checked line by line against it for correct built in names, argument names and behaviour. They were not executed, because that requires the hosted platform, and nothing on this page should be read as a claim that they were run.

All results are illustrative and simulated. They are not a track record, they are not a forecast, they are not a recommendation to trade any rule, and they are not an indication of what any strategy would produce in a live account. The purpose of the exercise is to demonstrate how much a testing convention can move a result, which is a property of the testing procedure rather than of any particular market.

Related

Continue reading

Next step

Find your starting stage. Everything else follows from there.

Educational reference only. No buy, sell or hold recommendations. All results shown are illustrative and simulated.