request.security Lookahead: Backtest vs Live Trades
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.
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.
| Situation | What the function returns | Committed? |
|---|---|---|
| Historical bar, HTF already confirmed | Final close of that HTF bar | Yes |
| Historical bar, no fresh HTF confirmation yet | Previous confirmed close (default gaps_off) | Inherits the earlier commit |
| Realtime bar, HTF not yet closed | The current fluctuating close, recomputed every tick | No — only committed at close |
| Realtime bar, HTF just closed | Final close of that HTF bar | Yes |
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".
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 source | Final 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 / Sharpe | Systematically inflated | Close to random, sometimes worse | |
| What you can trace | Almost nothing — everything looks normal | Two backtests of the same script disagree |
The one pattern the docs actually mark as "recommended", and why it feels backwards
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:
// 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.
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.
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
request.security() calls?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.Symptom 2: same script, two backtests, two different results
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.Symptom 3: live fires signals denser than the backtest — several entries on one bar
indicator() or strategy()?strategy() is the first step.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 valuelookaheadis passed — no argument means the defaultbarmerge.lookahead_off. - For any call using
lookahead = barmerge.lookahead_on, every value insideexpressionneeds 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(), notindicator()— 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?
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?
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?
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?
Order functions and where the data comes from
Does using strategy.entry() instead of building my own alert message dodge this problem?
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
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- You Generated a Pine Script Strategy With AI — Here's What It Takes to Actually Trade It
- Why Pine Script Alerts Don't Remember Anything (and Why That Breaks Naive Automation)
- Pine Script for Beginners — Write Your First Strategy in 30 Minutes
- Dynamic Position Sizing for TradingView Webhook Bots: Percent-of-Equity vs Fixed Quantity
- TradingView Webhook Not Working? A Complete Debugging Flowchart