Skip to content

One request.security on another symbol: what a backtest needs

6 min read

A strategy that trades one market but filters on another needs that second market’s bars, and your chart’s CSV does not have them. What PineForge’s engine needs to run such a script, what it does when the bars are missing, and what the Docker image and the MCP servers supply.

Updated on 2026-10-04: since release 1.1.0, the pineforge-release image installs other symbols' bars for request.security from the JSON index that PINEFORGE_SYMBOL_FEEDS names, one feed per requested timeframe and none aggregated (engine #327, pineforge-release #23; see the engine's 1.1.0 changelog). The rest of this post is as published on 2026-10-02, for release 1.0.1.

Your strategy trades one stock but only buys while the Nasdaq is in an uptrend. On TradingView that is one line, and it runs, because TradingView has every symbol's bars. A backtester that runs on your CSV has only the chart's. Ask it for QQQ and it has three choices: refuse, stop, or quietly compute the filter on the wrong market.

PineForge takes the second choice, and only when it has to: give the engine QQQ's own bars and the script runs.

The line that needs a second market

A script like this one only goes long while QQQ is above its 50-day average:

//@version=6
strategy("QQQ regime filter", overlay=true)
qqqUp = request.security("NASDAQ:QQQ", "D", close > ta.sma(close, 50))
fast = ta.sma(close, 20)
if ta.crossover(close, fast) and qqqUp
    strategy.entry("L", strategy.long)
if ta.crossunder(close, fast)
    strategy.close("L")

Line 3 is the problem. Its expression, close > ta.sma(close, 50), runs on QQQ's own daily bars: QQQ's closes, QQQ's 50-day average. No amount of aggregating the chart's bars produces those. Until recently, PineForge refused the script outright. It now runs it, as long as whoever runs the engine hands it QQQ's bars.

What the engine needs

The engine's public C header has one call for it (pineforge.h):

PF_API int strategy_set_symbol_feed(pf_strategy_t s, const char* key, const char* timeframe,
                                    const pf_bar_t* bars, const int64_t* close_ms, int n);

Read it as three requirements. The key is the exact string the script passes, NASDAQ:QQQ, so the bars are matched to the call that asked for them. The timeframe must be the one requested: a daily request needs daily bars, and a feed of the same symbol at another timeframe does not count. And close_ms gives every bar its own close time, because "no calendar is guessed for the symbol": the engine does not know when Nasdaq closes unless the bars say so. Facts such as the symbol's session, currency and tick size are optional, through a sibling call.

Whose clock

Close times matter because two markets close at different hours. On each chart bar, the engine hands the request the last bar of the other symbol that has closed by the chart bar's close (or, with lookahead on, the last one that opened by its open) (pineforge.h). So "a daily bar that closes after the chart's session close is seen one chart day late, as TradingView sees it" (MTF guide).

That is a TradingView rule, not a PineForge shortcut. The engine's tests keep a TradingView export of a gold daily chart that requests the dollar index. Gold's daily bar that opens on Sunday evening closes at 17:00 ET on Monday; the dollar index's Monday bar closes at 19:00 ET, two hours later. So on TradingView that gold bar reads Friday's dollar-index bar, and Monday's arrives with the next gold bar (fixture notes). A backtester that hands a chart bar the other market's bar before that bar has closed is reading the future.

When the bars are missing

The script still transpiles, with a warning that the request reads "another symbol's bars" and that "with none installed, the run stops with an error where its value is read" (codegen). At the first read, the run stops with a message that names the call (source):

request.security("NASDAQ:QQQ", "D", ...) at line 3: no data is pinned for this request, and its value was read

What it never does is read the chart's bars in QQQ's place (changelog). A filter computed on the wrong market produces a plausible equity curve that means nothing, so stopping is the safer failure. Two cases do not stop: a request whose value is never read (say, behind an input left off), and one whose value only reaches plots, alerts or tables, which becomes na with a warning that "trades are unaffected" (codegen).

What the Docker image and the MCP servers supply

Each of them backtests one market's bars: the chart's.

  • The Docker image takes strategy.pine and one ohlcv.csv (README), and its harness installs no other symbol's bars. The QQQ script stops there with the message above, and the image exits with code 4.
  • The local MCP server is built on that image and backtests one CSV. Its README says so: it "installs no other symbol's bars … request.security on another symbol, for example, is supported by the engine, but here a request whose value can reach a trade stops the run" (README).
  • The hosted MCP server takes a symbol, an interval and a venue, and fetches that one market's bars itself: crypto only, so a stock strategy like the QQQ example cannot run there at all (same README).

One trap catches people here: the Docker image and the local server never tell the engine which market the chart is, so a symbol written as a string counts as another symbol, even when it names the chart's own market (coverage notes). For the chart's symbol, write syminfo.tickerid. A higher-timeframe request on it needs no extra data, because the engine builds those bars from the chart's (MTF guide). One limit: on an intraday chart of a US stock or a CME future, TradingView's daily bar is the exchange's own, with the official close, which no aggregation reproduces (pineforge.h).

What this means for you

  • Filtering on the chart's own market? Use syminfo.tickerid, and the script runs on your CSV alone.
  • Filtering on another market? Run the engine yourself and install that market's bars, with their close times, before the run. The engine repo's test harness is a worked example: it installs them from a per-script requests manifest, whose fields scripts/run_strategy.py defines.
  • Comparing with TradingView? With the default lookahead off, expect a daily bar from a market that closes after yours to arrive a day late. That is TradingView's behaviour, and the engine matches it.

Checked on 2026-10-02 against PineForge engine and codegen 1.0.1, the releases of that day; this behaviour first shipped in 1.0.0.

Where to go from here