|
PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
|
PineForge exposes two distinct multi-timeframe surfaces. They look similar from Pine but resolve through completely different runtime paths, and they have different rules:
| Surface | Pine call | Direction | Bars source |
|---|---|---|---|
| Upward (HTF) | request.security(sym, "60", expr) | target TF coarser than input | aggregated from input bars |
| Downward (LTF) | request.security_lower_tf(sym, "1", expr) | target TF finer than the chart | the input's (or an auxiliary feed's) real bars when the target is not finer than them, else synthesized from each bar's OHLC path |
| Another symbol | request.security("TVC:DXY", "15", expr) | any TF | the other symbol's own bars, installed before the run |
The downward path is where PineForge can diverge from TradingView. TV reads real bars of the requested lower timeframe; PF reads real ones only when the host supplies them — an input finer than the chart (input_tf < script_tf) or an auxiliary feed (strategy_set_aux_security_feed). A target finer than every bar the run was given is synthesized on demand by walking the input bar's OHLC path. See Bar magnifier for the same idea applied to fill resolution.
For the upward path and the chart-aggregation rules, see also Timeframes.
A request.security of another symbol reads that symbol's own bars; no aggregation of the chart can produce them. The host installs them before the run, keyed by the exact symbol string the script passes and by the timeframe, each bar with its own close:
On each chart bar the site sees the last bar of the other symbol that has closed by the chart bar's close (lookahead_off) or that opened by the chart bar's open (lookahead_on); no calendar is guessed for the other symbol, so a daily bar that closes after the chart's session close is seen one chart day late, as TradingView sees it. The site's expression runs on every bar of the other symbol, in order, so its history offsets and indicators run over that symbol's own bars; inside it bar_index, time_close and syminfo.* are the other symbol's. gaps_on reads na on a chart bar that received no new bar; the generated clear_security() that blanks the site there also clears the expression's own history series, as it does for a same-symbol site, so under gaps_on an offset or an indicator inside the expression starts over after each such bar (a codegen limit, not the merge's). A symbol with no installed feed fails the run closed, naming the symbol and the timeframe; a feed installed with no bars reads na on every chart bar, and an invalid symbol reads na under ignore_invalid_symbol. Historical runs only: a stream refuses an installed symbol feed, and every setter answers -1 while a run is in progress. The merge itself is the kernel's (NativeRunSpec::instrument_feeds, Native engine) and holds for a native host too.
scripts/run_strategy.py installs a probe's pinned feeds, facts and recorded request values (strategy_set_recorded_series, TradingView's per-chart-bar fundamentals; a tape with a header only is a request na on every chart bar) from PINEFORGE_REQUESTS_ROOT/<probe>/requests.json when that variable is set. pineforge-codegen 1.0.0 lowers a foreign request.security onto this surface; its 0.10.4 release refuses a symbol that is not the chart's.
Every backtest run takes two timeframe strings on run_backtest_full(...):
The runtime resolves them in order:
input_tf — what the bar feed actually is. If empty, the runtime computes the median delta between consecutive pf_bar_t::timestamp values and snaps to a canonical Pine TF.script_tf — what the strategy script believes it's running on. If empty, defaults to the resolved input_tf.script_tf > input_tf, the runtime aggregates input bars up to script-TF parents before each on_bar dispatch. The two timeframes are concatenated by an aggregator (ratio or calendar — TimeframeAggregator, src/timeframe.cpp).The report exposes the resolved values and the ratio:
| Report field | Meaning |
|---|---|
input_tf_seconds | Resolved input TF (after auto-detect). |
script_tf_seconds | Resolved script TF (after defaulting). |
script_tf_ratio | script_tf_seconds / input_tf_seconds. |
needs_aggregation | 1 when ratio > 1, 0 otherwise. |
The full runnable harness — three tables walking the script_tf sweep, the input_tf/script_tf pair matrix, and the lower-TF synthesis ratio — is in tutorial/run_mtf.py. See the Worked example section below.
The lower-TF builtin returns an array of one value per synthesized sub-bar. The codegen pattern in generated.cpp is:
register_security_eval, register_security_lower_tf_eval, security_lower_tf_sub_bar_index and security_series_slot_is_new are the Pine source host's (source::PineStrategyHost, src/source/pine_security_eval.cpp). The kernel underneath registers a plain aggregating evaluator and keeps the feed store; every request.security rule on this page — lookahead and gaps publication, Heikin-Ashi, the lower-timeframe emulation, the diagnostics below — is source-host state kept per sec_id beside it.
When the run begins, the source host validates each registered lower-TF site against the run's evaluator input timeframe (validate_security_timeframes):
lookahead and gaps must be off (TV does not expose them on this builtin and PF refuses to fake them).Violations raise at run-time with a precise diagnostic, e.g.:
For each input bar, the engine generates input_tf / target_tf synthetic sub-bars by walking the input bar's OHLC endpoints (the same path-sampling primitive the bar magnifier uses, see src/engine_lower_tf.cpp). Each synthetic sub-bar gets 1/ratio of the input bar's volume.
Per-run sub-bar counts surface in the report's security_feeds_total field. With a 15m chart and a "1" lower-TF target, expect:
tutorial/mtf/ ships two strategies side-by-side:
| File | Demonstrates |
|---|---|
tutorial/mtf/strategy_htf.pine + generated_htf.cpp | request.security (HTF SMA filter on a 15m chart). HTF is an input — sweepable without rebuild. |
tutorial/mtf/strategy_ltf.pine + generated_ltf.cpp | request.security_lower_tf("1", close) — intra-bar 1m range as an entry signal. |
tutorial/run_mtf.py | Three tables: script_tf sweep, input_tf/script_tf pair matrix, lower-TF synthesis ratio. |
Build:
Expected (excerpt):
Numbers depend on the OHLCV snapshot.