Appearance
Leak-safe AmenoScript
How do you know a backtest didn't read the future, and that the script will act live as it did in history? Write it in AmenoScript. Every AmenoScript run is leak-safe, and no setting or header makes any other run so. Before the run starts, the engine refuses anything in the text that could read a value before it was known or act differently on a live bar. While it runs, it gives your code only closed bars and checks every read against when that bar was known.
ameno
indicator("Last closed week", overlay = true)
// max_age is AmenoScript's, so this line makes the text leak-safe
float week = request.security(syminfo.tickerid, "1W", close,
max_age = 14 * 86400)
plot(week, "Weekly close", color.orange)Each chart bar here reads the close of the last week that had closed by that bar's close, in history and live alike, with no lookahead argument. max_age is the line that makes the text AmenoScript. It is in seconds, and turns the read to na once the week it finds is more than 14 days old (Facts). Leave it out and the text is Pine: history reads the same, but a live chart reads the week still forming. Add barmerge.lookahead_on to that Pine line and every bar shows its week's final close from the week's first day.
Which scripts run leak-safe
A script runs leak-safe when its text is AmenoScript, that is, when it uses anything only AmenoScript has. The engine reads that off the text with its libraries linked in, so a library that uses an addition makes your script leak-safe too. AmenoScript says how the dialect is read.
A Pine Script text runs as TradingView runs it. The engine still reads it for the same leaks and reports each as a warning, as How a Pine script hears it shows. You can't switch leak-safety off for an AmenoScript text.
What it refuses
These constructs stop an AmenoScript text before its first bar. Each refusal starts with Leak-safe AmenoScript refuses this: and names the line.
| Construct | Why it's refused | Write instead |
|---|---|---|
barmerge.lookahead_on, or lookahead = on request.security, request.dividends, request.splits or request.earnings | It reads a bar's final value before the bar closes. | Leave it out. request.security already reads the last bar closed by this bar's close. |
varip on a variable or a type's field | It keeps a value across a live bar's updates, which a historical bar never has. | var |
calc_on_every_tick = true | It decides on a live bar's updates, which history doesn't have. | Nothing: your code runs once a bar, at its close. |
calc_on_order_fills = true | It decides again on a fill inside a bar, which history can't replay. | Nothing. |
calc_bars_count on the declaration, request.security, request.security_lower_tf or request.seed | It counts bars back from the newest, so the bar a run starts on moves as new bars arrive, and every value that depends on the start moves with it. | Leave it out: every bar runs. |
A negative constant offset on plot, plotshape, plotchar, plotarrow, bgcolor or barcolor | It draws a bar's value on bars before that bar. | A zero or positive offset. |
hindsight = true on request.universe | It reads each coin's newest tags on every date. | Leave it out. Universes explains the tags. |
| A decision that rests on a live-only name | It acts differently live than in history. | See Live-only names. |
ameno
indicator("Tick counter")
varip int ticks = 0 // refused: use var
ticks += 1
plot(ticks, "Ticks")
// request.age is AmenoScript's, so this text is refused, not warned
plot(request.age(syminfo.tickerid, "1W"))A negative offset that comes from an input can't be seen in the text, so the run stops on it when it happens.
Your code runs on closed bars
A leak-safe run runs no code of yours on the forming bar, the bar still open on a live chart. Your last bar is the last closed one, so barstate.islast, last_bar_index and last_bar_time read that bar. The forming candle has no plot point, no table update and no new order.
The strategy emulator still walks the forming bar. An order your code placed at the last close fills there as it would in history, at the bar's open or when its price is touched. An alert() or alertcondition() runs with the rest of your code, so it fires at a bar's close, never while the bar forms.
The forming bar is blank
A leak-safe study's lines stop one bar short of the live candle. That is the bar your code hasn't seen close yet, so there is nothing to draw on it.
How a run reads other series
Every series has a known time for each of its bars: the time its value could first be seen. For a market's bar that is its close. For a dated series, such as a dividend, an economic release or a day's coin ranking, it is when the value was published. Facts describes dated series.
A leak-safe read of another series gives the last bar known by the close of the chart bar reading it:
request.securityon a higher timeframe reads the last closed period, never the one forming, live or in history.request.security_lower_tfhands over the intrabars known by this bar's close.- A dated series reads the newest row published by this bar's close, and na before its first one.
- A value with no known release date reads na, because a guess could show it early.
A share's bars come from a feed that adjusts old prices for every split up to today. A leak-safe run on a share therefore warns that its old prices hold splits that came later. The run goes on, and the warning stays in the Strategy Tester.
LeakError
The engine checks every read against its known time while the script runs, on all four engines: the interpreter, the JIT, the Rust walker and Rust native (The engines). A read that would see a bar before it was known stops the run with a LeakError that names the read, when its bar was known and the chart bar that asked.
You shouldn't meet one. The refusals above stop every construct that could cause it, so a LeakError means the engine itself let a read through early. Report it with the script.
Live-only names
Some builtins read where the data ends or what the clock says, so the same bar reads them one way live and another way in history. They are the live-only names:
| Name | Why it's live-only |
|---|---|
barstate.islast, barstate.isrealtime, barstate.ishistory, barstate.islastconfirmedhistory | True on different bars as new data arrives. |
last_bar_index, last_bar_time | Move with every new bar. |
timenow, timestamp() with no arguments | Read the clock. |
chart.left_visible_bar_time, chart.right_visible_bar_time | Follow the view. |
math.random() with no seed | Differs on every run. |
A live-only name can draw. A table or a label takes one silently. A plot's value or colour takes one with a warning, because the plot's past bars then show otherwise than they did live. A live-only name can't reach a decision, and a decision is any of these:
- an order:
strategy.entry,strategy.order,strategy.exit,strategy.close,strategy.close_all,strategy.cancel,strategy.cancel_allorstrategy.risk.*; book.targetorbook.config;alertoralertcondition;- a report row:
report.columnorreport.matrix; - a plot that something acts on: one titled
Target Weight, which the terminal reads as a strategy's target weight, or one the Screener reads to pass a symbol, titledmatch,signal,filter,screenorpass.
The check follows a value wherever it goes: through variables, a function's arguments and what it returns, a type's fields, arrays and maps, and conditions. A live-only name that decides whether an order runs is caught as surely as one passed to it.
ameno
indicator("A target on the last bar")
array<string> names = array.from("BTC-USD", "ETH-USD")
if barstate.islast // decides whether book.target runs: refused
book.target(names, array.from(0.5, 0.5))
plot(book.equity())A table filled on the last bar is fine, because a table decides nothing:
ameno
indicator("Last weekly close in a table")
var table t = table.new(position.top_right, 1, 1)
float week = request.security(syminfo.tickerid, "1W", close,
max_age = 14 * 86400)
if barstate.islast
table.cell(t, 0, 0, "Weekly close " + str.tostring(week, "0.00"))
plot(week, "Weekly close")How a Pine script hears it
A Pine Script text runs as TradingView runs it, and each refusal above comes back as a warning that starts with Leak warning:. A lookahead_on warns only where it really reads ahead: with an offset, as in close[1], it reads the last closed period, so it gets no warning.
pine
//@version=6
strategy("Enter on the last bar")
// Runs, with a warning: strategy.entry depends on barstate.islast
if barstate.islast
strategy.entry("Long", strategy.long)
plot(close)A Pine text whose decisions rest on a live-only name says so wherever it is read: in the Script Editor, the Strategy Tester, the chart's legend, the Optimizer and the Screener's replay.
How the guarantee is tested
The refusals catch what a text says. The engines are also tested on what a run does. Every script in their test corpus, and many random scripts written to probe them, runs cut short at sampled bars. Each cut must equal the whole run up to that bar on every plot, fill, working order, target, alert and report row. A cut is tried four ways:
| Cut | What it sees |
|---|---|
| History | The bars as they were known at the cut, the last one closed. |
| Live | The same, plus a forming bar, as a live chart has. |
| Another future | The bar's past as it was, then a later history that didn't happen, so a read past the bar shows. |
| A close | What a run made at a bar's close is handed, as an alert's run is. |
A cut that differs in history or in another future shows a leak: the run read something it couldn't have known. One that differs only live or at a close shows a repaint, where history holds but a live chart would act otherwise. In a leak-safe run either is an engine bug, fixed on all four engines.