Automation Gotchas

request.security Lookahead: Backtest vs Live Trades

2026-09-14·13 min read

You describe "go long on the 1H when the 4H RSI drops to 30" to an AI. It hands you back a Pine Script strategy — the scripting language TradingView charts use. The request.security line looks clean, you paste it into a chart and click Add to chart, and the backtest freezes you in your seat the moment it renders: 92% win rate over a year, Sharpe 3.1, 4% max drawdown.

You wire the alert up, point it at your execution server's webhook (the HTTP channel that carries TradingView signals back to your own server), put real money behind it. Three days in, only two signals fired — both went the wrong way. On the fifth day you rerun the same script and the equity curve doesn't look like the one you saw last week. You check the alert, the payload, the server clock — all fine.

What's actually broken is the lookahead argument on that request.security() line. Its behavior is the opposite of what the name suggests. TradingView's official Pine Script v6 documentation places a big all-caps warning in the middle of the section describing that parameter — buried deep in the English docs, and the AI won't translate it for you when it generates code. So this post isn't about teaching you to write Pine. It gives you three things: the mechanism behind the failure, the questions to send back to the AI, and a checklist you can verify yourself.

Bottom line up front: whenever request.security() is called with barmerge.lookahead_on and the expression isn't paired with a historical offset like [1], that higher-timeframe data effectively "sees the future" on historical bars. The backtest reads the high, low, and close of an HTF bar that hasn't finished yet — data you cannot access live. TradingView's own docs call the resulting backtests "unrealistic" in that parameter's section, and the script-publishing rules flatly say such scripts are "not allowed as script publications".

What request.security actually does — split it into two layers first

On a 1-hour chart you write request.security(syminfo.tickerid, "240", close) and your gut reads it as "give me the 4-hour close." The literal reading is fine, but it hides two things — and those two things are what fails.

Thing one: HTF data doesn't produce a new value on every chart bar

Following the official docs' own example, if you request 1-hour data on a 1-minute chart, the function only returns a new value on the 1-minute bars that cover a 1-hour bar's open or close. What the other 1-minute bars receive is controlled by the gaps parameter. Leaving gaps unspecified means the default barmerge.gaps_off: on historical bars the function fills the gap with the last confirmed value, and on realtime bars it fills with the latest fluctuating value.

Thing two: the docs are explicit that historical and realtime bars behave differently

On historical bars, request.security() only returns a new historical value when the requested timeframe's bar closes and is confirmed. On realtime bars, it recalculates and returns a value on every chart-bar update, and only "commits" that value once the requested bar actually closes. So the "live 4H close" you see on the chart is a temporary number that moves with every tick.

SituationWhat the function returnsCommitted?
Historical bar, HTF already confirmedFinal close of that HTF barYes
Historical bar, no fresh HTF confirmation yetPrevious confirmed close (default gaps_off)Inherits the earlier commit
Realtime bar, HTF not yet closedThe current fluctuating close, recomputed every tickNo — only committed at close
Realtime bar, HTF just closedFinal close of that HTF barYes

Default behavior of request.security() on historical vs realtime bars. Source: TradingView Pine Script v6 official documentation, 'Other timeframes and data' — the 'gaps' section and the 'Historical and realtime behavior' section (verified August 2026).

Put those two things together: the backtest is running on "historical bars + committed HTF values." Live, before the HTF bar closes, you're running on "a fluctuating value that can roll back." Those are already two very different signal sources. The lookahead trap is what happens when that structural gap is amplified one more layer.

What does the lookahead flag actually toggle? The answer contradicts intuition

lookahead takes two legal values: barmerge.lookahead_off (default) and barmerge.lookahead_on. The name suggests "should I look ahead," as if it were a matter of performance or style.

What the docs actually define this parameter as

When you request HTF data, lookahead decides whether the function, on historical bars, can access values from moments after the bar in question actually occurred — that is, whether this data series carries lookahead bias on the historical side.

In plainer terms: lookahead_on lets the function, during historical replay, read the final value of an HTF bar — the value only known after that bar closes — on the sub-bars that fall before the HTF bar has actually finished.

The all-caps warning from the docs, quoted verbatim

The docs place a warning in that same section: "Programmers should exercise extreme caution when using lookahead in their requests, especially when requesting data from higher timeframes." A Notice follows, saying scripts that use lookahead to leak future data into history "are extremely misleading. As such, they are not allowed as script publications," because "the retrieved data was not knowable at the time of each bar. Furthermore, the same behavior is impossible to reproduce on realtime bars".

This paragraph carries more weight than its placement suggests
The quoted passages are verbatim from the TradingView Pine Script v6 documentation, section on lookahead (verified August 2026). "Not allowed as publications" is a publication rule, not a compile-time error. Your script still compiles, still backtests, still lets you create an alert, still fires a webhook. TradingView blocks public publication of such scripts; it does not block you from trading them yourself. That's what makes this trap so insidious: every light on the path is green — only the result is fake.

Why you barely notice this on historical bars, and why it detonates live

The same script producing two very different equity curves over two different time windows is itself the signature of this bug. The reason is that under lookahead_on (with no offset), the signal source in the historical and realtime states is not the same thing at all.

Historical state: the strategy is reading a close that hasn't happened yet

On every 1-hour bar, the strategy can read the final close of the 4-hour bar containing it — even if that 4-hour bar isn't over. That's like standing on a 1-hour bar and knowing the close that only gets set four hours later. This informational edge systematically pushes entries onto the correct side, and of course the backtest looks beautiful.

Realtime state: the signal source is gone, and it will be overwritten

The 4-hour bar hasn't closed yet. TradingView does not have a time machine. Even with lookahead_on, the most live trading can grab is the "current fluctuating close," which moves with every tick. And once the script reloads, the previous realtime bars turn into historical bars — which lookahead_on then overwrites with the final post-close value. That is why running the same script five days later paints an equity curve that doesn't match what you saw last week.

The example script description in the official lookahead section says the same thing: unshifted lookahead_on "repaint their results after the user reloads the script".

This is the failure most likely to be misdiagnosed as a webhook or execution-side problem. You'll re-check whether the alert fired, whether the payload arrived, whether the server returned 200. All of those come up clean — because the alert logic ran what the strategy told it to run. The strategy just happened to see two different worlds in backtest and live.

Backtest (historical bars)Live (realtime bars)
Signal sourceFinal close of the HTF bar (future data)Current fluctuating close, changes every tick
Repaints?No (history overwritten to final values)Yes — prior segments get overwritten again after script reload
Win rate / SharpeSystematically inflatedClose to random, sometimes worse
What you can traceAlmost nothing — everything looks normalTwo backtests of the same script disagree

This part is counter-intuitive, but do it anyway: to fix it, keep lookahead_on and add a [1] historical offset to the expression.

From the docs: "The most reliable approach to achieve non-repainting results is to use an expression argument that only references past bars (e.g., close[1]) while using barmerge.lookahead_on as the lookahead value."

Why "keep it on, add [1]" instead of simply turning it off

Turning it off gives you "the last confirmed HTF bar's close, but with the value updating on the HTF-close timing." That's reasonable on historical bars, but you'll find that after a script reload the live segment's signal positions no longer line up with the historical segment.

The combination of [1] plus lookahead_on does one consistent thing: always take "the previous confirmed HTF bar," with the value-update timing pinned to the moment the new HTF bar opens, in both history and live.

The docs put this plainly too: "applying an offset to the expression effectively prevents the requested data from repainting when the script restarts its executions and eliminates lookahead bias in the historical series". The htfPrices() library example on the same page ends with exactly this pattern:

pine
// TradingView's official htfPrices() example (Pine Script v6 docs, 'In libraries' section)
        request.security(
          tickerID,
          timeframe,
          [open[1], high[1], low[1], close[1]],
          lookahead = barmerge.lookahead_on
        )

The price you pay: signals will lag one HTF bar

All your HTF signals will now come one bar late — where you used to enter on "this 4-hour bar's close," you'll enter on "the previous 4-hour bar's close." That's the price the fix costs, and it's why plenty of forum posts refuse to write it this way: once you fix it, the backtest numbers slide from the set that froze you in your seat toward something close to random. That gap is exactly what the lookahead bias was quietly doing for you.

Is there any other usage that's acceptable?
The docs list one exception in the same section: "The `expression` argument in a request.security() call includes a historical offset (e.g., close[1]), which prevents the function from requesting future values that it would not have access to on a realtime basis." In other words, as long as the expression itself is already offset — even if only because it's wrapped in a form like ta.wma(close, length)[1]lookahead_on is acceptable. The test is "could this expression actually be obtained in real time," not "does the return value happen to be na."

Paste these five questions back to the AI when it hands you a multi-timeframe strategy

Same move as the one in the AI-generated Pine strategy post: don't let it edit yet. Make it answer, item by item, first. These five are deliberately designed to make it out itself — a script that hasn't stepped on this trap will answer question 1 completely, and the rest will go fast. A script that has stepped on it will start hedging from question 3 onward.

text
This is a TradingView Pine Script v6 strategy that will fire a webhook to a live execution service.
        Before you change a single line, answer each of the numbered questions below, then give me a corrected version.

        1. For every request.security() call in this script,
           list them one by one, and for each one state:
           (a) the value of the timeframe argument
           (b) the value of the expression argument
           (c) whether lookahead is passed, and if so which value (barmerge.lookahead_on / off)
           (d) whether the expression uses a historical offset (e.g. close[1], high[1])

        2. For any call where (c) is barmerge.lookahead_on and (d) is "no",
           explain what value the call returns on historical bars vs realtime bars,
           and whether those two are data from the same point in time.

        3. Following on from question 2: if I add this script to a fresh chart, click Add to chart,
           and then reload it, will the signal positions from the previous backtest render get overwritten?
           If so, which call(s) cause it?

        4. I also have an alert routing to my execution server via webhook.
           While the HTF bar is still open, will my execution server receive a signal?
           If so, is the basis of the signal at that moment the same as the basis I would see
           looking back on that bar after it closed?

        5. If I change every request.security() to "lookahead = barmerge.lookahead_on
           plus a [1] historical offset on every value inside expression",
           which signals disappear? Which signals show up one HTF bar later? List them one by one.

        [Paste your strategy here]

Question 3 is the core of this round. A good answer will state directly, "yes, it will be overwritten, because barmerge.lookahead_on has no offset." If it comes back with "no, Pine Script is deterministic so reloads produce the same result" — that's literally true (the same code plus the same history produces the same result) but it dodges the actual event that "prior realtime bars are now historical bars." That kind of answer earns extra skepticism.

Question 5 is there to estimate the signal-density change after the fix. If it can't answer, it probably didn't really understand what it just wrote — at which point don't ask it to edit; make it start over.

Match your backtest-vs-live gap to the mechanism

Chaining everything above together, most "beautiful backtest, broken live" reports map onto the mechanisms below. These three symptoms cover the lookahead trap and its close relatives only — if your problem isn't among these, it may be alerts that have no memory, or a webhook qty_type sent incorrectly.

Symptom 1: gorgeous backtest, live diverges within the first week

1
Does your strategy have any request.security() calls?
Yes, and lookahead = barmerge.lookahead_on without a [1] offsetClassic lookahead-bias symptom. Historical bars are reading the HTF close-after-the-fact; live cannot access it. Apply the fix from the previous section: keep lookahead_on and add [1] to the expression. Backtest numbers will fall after the fix — the drop is what the bias was doing for you.
No multi-timeframe calls, or already following the recommended patternLookahead isn't the main suspect. Check elsewhere: alert Frequency set to Once Per Bar with the condition reading an unclosed close, wrong qty_type on the webhook, or lack of deduplication on the execution side.

Symptom 2: same script, two backtests, two different results

1
Between the two backtests, did any realtime bars accumulate in between?
Yes (for example a few days passed)Previous realtime bars are now historical bars and have been overwritten by lookahead_on to the HTF post-close value — the signal positions moved. The example description in the official lookahead section spells this out. Switching to the recommended pattern makes reloads leave the historical segment alone.
No, but the data source or exchange changedDifferent exchanges take slightly different snapshots of "the same bar's close at the same instant" — that's a data-source-layer issue, unrelated to lookahead. Pin the source to one venue before comparing.

Symptom 3: live fires signals denser than the backtest — several entries on one bar

1
Is your strategy declared with indicator() or strategy()?
indicator()Indicators recalculate on every tick by default; strategies compute once per closed bar by default. Multi-timeframe + indicator + a condition reading an unclosed value produces signals live that don't match the backtest. Switching to strategy() is the first step.
Already using strategy() but still firing every tickCheck whether calc_on_every_tick or calc_on_order_fills is set; either one re-runs the strategy on every tick during realtime, effectively re-layering the indicator behavior on top.

Pre-launch self-check list

Before you point this strategy at real money, walk through the list below. None of the items require you to write Pine — either flip a dropdown in the browser, or ask the AI one specific question:

  • List every request.security() call in the whole script, and for each one confirm what value lookahead is passed — no argument means the default barmerge.lookahead_off.
  • For any call using lookahead = barmerge.lookahead_on, every value inside expression needs a [1] (or [n]) offset. In the tuple form, every element has to be offset — missing one is the same as missing all of them.
  • Declare with strategy(), not indicator() — multi-timeframe + indicator will almost always produce a live-vs-backtest mismatch.
  • Save a screenshot of the backtest. One week later, rerun the same script over the same window and compare — signal positions or equity-curve shifts are the most direct evidence of repaint.
  • Set the alert Frequency to Once Per Bar Close, or gate the condition in code with barstate.isconfirmed. This isn't the lookahead trap itself, but it often stacks on the same symptom (see alerts that have no memory).
  • Route the backtest results through your execution side in dry-run and diff them, before putting real money behind it — this step catches more than just lookahead-class bugs.

Honest paragraph: fixing this doesn't mean the strategy will make money

Once the lookahead bias is fixed, the backtest numbers slide toward random — that in itself is not a bug. Those are the numbers the live version can actually reproduce. Some strategies have no edge after the fix, and that isn't the fix's fault; the original edge only existed under the cheat of knowing the 4-hour close in advance. We can't decide for you whether to abandon such a strategy. What we can do is make the numbers honest first, so you at least have something real to base the decision on.

There are also several things Pine Script itself cannot fix: it can't know whether your webhook was received downstream, can't deduplicate, can't track the actual exchange fills. That's the execution side's job. TVSBot's approach is to store every signal alongside its raw payload and processing record, per-signal replayable, so that when you're reconciling backtest against live, you can actually inspect what got sent. But to be clear: that's an after-the-fact trace, not an automatic lookahead fix. Fixing the strategy's own bias means going back to the checklist above.

FAQ

If I use lookahead_off (the default), am I completely safe?
Mostly safe, but there's still the fluid data problem to handle. The docs are explicit in the same page's gaps section: even under lookahead_off, on a realtime bar the function still returns "the current fluctuating close" — a value that moves with every tick. To fully eliminate that layer, offset the expression with [1], or gate the condition with barstate.isconfirmed.
A lot of forum scripts use lookahead_on without a [1]. Are they wrong?
In most cases, yes. TradingView's script-publication rules classify that pattern as "not allowed as script publications", on the grounds that it "is not realistic and cannot be reproduced live." There is one exception to watch: if the author's expression is already offset somewhere else (e.g. wrapped as ta.wma(close, length)[1]), lookahead_on is acceptable. Checking call by call whether the expression carries an offset is the safest test.

Old scripts and scripts you don't own

What about legacy Pine v1 or v2 security() calls I inherited?
Treat them as if lookahead_on were set. The docs contain a Notice in the lookahead section: "In Pine Script versions 1 and 2, the security() function did not include a lookahead parameter. However, the request behaved the same as those with lookahead = barmerge.lookahead_on in later versions of Pine" — so any legacy HTF security() call is suspect unless the expression already has an [] offset.
If I can't modify the script (say I bought the strategy from someone), can the execution side block this?
The execution side can't reverse-engineer whether "those backtest signals were computed using future data." It only sees the live payloads that get pushed to it; it doesn't have the backtest computation. A rough workaround: keep two records of the same script — one from the live alerts arriving over time, another from a replay backtest of the same window — and compare their signal positions. Positions that don't align are evidence of repaint. That's detection, not fixing.

Order functions and where the data comes from

Does using strategy.entry() instead of building my own alert message dodge this problem?
No. The trigger conditions for strategy.entry() are still the if-clauses in your script that read HTF data. If the judgment itself was made from data that leaked from the future, the order call is executed against a leaked signal. This bug is in the data source, not in how you dispatch the order.

Get started

Ready to ship what you just learned?

TVSBot turns TradingView alerts into orders across 7 exchanges — non-custodial, your own API keys, dry-run first before real money, and every signal is replayable for verification. Root-cause bugs like lookahead have to be fixed back in Pine, but we keep the record of every signal that flows through.

Start free