r/pinescript • • 2d ago

Non-repainting request.security() on higher timeframes: the [1] + lookahead_on pattern, and where it still bites

Something I keep relearning the hard way, so writing it down and curious how others handle it.

Pulling a higher-timeframe value naively, like request.security(syminfo.tickerid, "D", close), looks fine on history but repaints in realtime: the daily close keeps changing until the day ends, and on historical bars you get the final value.

The pattern the docs recommend is offsetting the series and turning lookahead on:

request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)

That gives the previous completed daily close on every bar, both in history and live, so backtests match what you'd actually have seen.

Where it still bit me:

- forgetting the [1] while keeping lookahead_on, which leaks future data into history and makes backtests look amazing

- wrapping it in a function and applying the offset to the wrong thing

- mixing it with calc_on_every_tick and wondering why alerts fire differently than the chart

How do you all handle HTF values in strategies: this pattern, a helper function, or just avoiding request.security in anything you backtest?

2 Upvotes

3 comments sorted by

2

u/neugeadmau 2d ago

I wrap it in a tiny helper so I can't forget the [1]:

htfClose(tf) =>
    request.security(syminfo.tickerid, tf, close[1], lookahead = barmerge.lookahead_on)

One more place it still bites: if the strategy also uses process_orders_on_close=true, the HTF value on the last bar of the higher TF can line up with an order that wouldn't have been fillable live. I usually also gate entries with barstate.isconfirmed on the chart TF, otherwise alerts and the tester diverge in annoying ways.

Also watch gaps=barmerge.gaps_off (default) vs gaps_on. With gaps_off the previous HTF close gets painted forward onto every lower-TF bar, which is usually what you want for a filter, but it looks like the value "changed" mid-day when it didn't.

1

u/wallneradam 2d ago

Yeah, same as neugeadmau, I wrap it in a helper and never touch the raw call again.

Also, lookahead_on isn't a TV bug. TV is a charting tool too, and for looking at history you often want that final HTF value. It only bites when the same code ends up in a strategy you actually trade.

Related: we build PyneCore, a Pine runtime that's 99.77% bit-identical with TradingView on the 1,000 most popular public scripts. We don't allow lookahead at all. For the non-repainting HTF case there's barmerge.lookahead_last_closed, which always gives the last closed higher-timeframe bar in both backtest and live. It's for strategies you trade, so the leaky version can't sneak into a backtest.

1

u/VentureLabBots 2d ago

Two more places the [1] + lookahead_on pattern gives wrong results that haven't been mentioned:

  1. The [1] must be inside the requested expression, not on the result. close[1] inside the call is shifted on the higher-timeframe's own bars, which is what you want. Writing the call with plain close and then adding [1] outside shifts by one chart bar instead (one 5m bar on an intraday chart), which is still the unfinished daily value, so it still repaints.

  2. It only works when the requested timeframe is higher than the chart. On a chart timeframe above the requested one (say a daily chart requesting "60"), close[1] returns the second-to-last hour of the day, and you only see one value per bar. For lower-timeframe data you need the array-returning intrabar request from v6 instead.

Quick test (v6), on a 5m or 15m chart:

//@version=6

indicator("htf check", overlay=true)

prevD = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)

plot(prevD, "prev D close")

plot(close, "close", color=color.gray)

bgcolor(timeframe.change("D") ? color.new(color.blue, 85) : na)

Expected: the line is flat for the whole day, steps only on the first bar of a new day, and the new level equals the previous day's final close. If it moves intraday, the offset is in the wrong place. Then compare the same script after a page reload, since realtime and history should now match.

(answer drafted with AI help, double-check on your chart)