Skip to content

Latest commit

 

History

History
1009 lines (809 loc) · 82.3 KB

File metadata and controls

1009 lines (809 loc) · 82.3 KB

PineScript v6 Runtime Coverage

What this page is. A complete map of the Pine v6 surface area that libpineforge.a actually implements: which features have a dedicated runtime class or function, which features are deliberately left to the consuming compiler, and which features are not supported anywhere in PineForge today.

Audience. Anyone using PineForge as a backend — building a custom Pine-to-C++ transpiler against this runtime, integrating PineForge into a strategy harness, or auditing what is actually covered before trusting the parity claim. Source-of-truth files: the headers under include/pineforge/ and the implementations under src/.

Two layers of "supported". PineForge as a whole = (a) this runtime plus (b) PineForge's separate, source-available PineScript-to-C++ transpiler (pineforge-codegen). Some Pine surface (arrays, UDTs, most scalar math.* calls) has no dedicated runtime class because the transpiler emits the implementation inline using the C++ standard library or generated structs. Maps span both layers: map.hpp provides the PineMap<K,V> runtime and the transpiler routes its supported map surface through that handle type. The transpiler also owns the conservative type/admission checks for map-bearing history, collection, and specialization boundaries. Where this distinction matters, the buckets below call it out explicitly.

Out of scope today. Plots, chart rendering and alert delivery are not implemented by this runtime regardless of consumer. Drawing objects (line, box, label, linefill, chart.point) are: drawing.hpp keeps their geometry as data a strategy reads back. The runtime accepts continuous ordered-trade streams, but it does not emit alert events.

Coverage summary

Category Runtime status What libpineforge.a owns
Engine / strategy lifecycle Supported BacktestEngine, one-shot run(...) overloads, continuous historical-to-realtime streams, bar loop, raw-trade broker passes, on_bar(...) hook, and cumulative reporting.
Strategy orders Supported strategy_entry / order / exit / close / close_all / cancel / cancel_all with OHLC-path fill resolution, OCA, pyramiding, slippage, commissions, margin gates, partial / FIFO-vs-ANY closes, trailing stops, and TV deferred-flip carry handling.
Strategy state / accessors Supported Position state, equity / drawdown / runup tracking, win / loss counts, full closed- and open-trade accessor methods, intraday fill counter.
Strategy risk Supported All six strategy.risk.* gates are wired (the Pine adapter's risk state + the fill/order gates): allow_entry_in direction allow-list, max_position_size, max_drawdown (abs / % of peak equity), max_intraday_loss (abs / % of equity), max_cons_loss_days, and max_intraday_filled_orders (latch-till-day-rollover cap-close).
Inputs Value support only unordered_map<string,string> injection plus typed getters (get_input_*). UI metadata is the consumer's problem.
ta.* Broad runtime support 59 official Pine v6 ta.* functions plus 8 official ta.* series variables backed by stateful runtime classes, and a free pivot_point_levels(...). Stateful classes expose both compute(...) (advance state) and recompute(...) (re-run on the same bar without permanently advancing history).
math.* Narrow runtime backing Runtime owns only deterministic pine_random(...) and rolling math::Sum; everything else is left to consumer-emitted code.
str.* Narrow runtime backing Runtime owns pine_str_format, pine_str_format_time, pine_str_match, pine_str_split, pine_str_tostring.
request.security() Partial Runtime owns the security state machine, ratio / calendar aggregation, lookahead / gaps semantics, lower-TF emulation, per-security diagnostics, and (lane XSYM-D) the merge of another symbol's installed bars, which pineforge-codegen 1.0.0 lowers a site of another symbol onto (0.10.4 refuses it).
Bar magnifier Supported TradingView's own intrabars, built from a feed finer than the chart (source/magnifier_intrabars.hpp); with the chart's own bars only, OHLC-path sampling with 6 distribution modes plus optional volume-weighted sample density.
Time / session / timezone Supported pine_time / pine_time_close with session filtering and a mutex-guarded tz_util::ScopedTimezone.
Timeframe parsing Supported tf_to_seconds, tf_ratio, tf_change, detect_timeframe, calendar boundary detection, TimeframeAggregator (passthrough / ratio / calendar).
Numeric matrices Supported PineMatrix over Eigen::MatrixXd — construction, access, transforms, linear algebra, predicates.
Typed matrices Supported PineGenericMatrix<T> (header-only template) for int / bool / string / color / UDT element types — structural ops only (numeric methods stay on PineMatrix).
Series history Supported Series<T> ring buffer with Pine [k] semantics.
Color Supported pine_color constants plus new_color, r, g, b, t helpers.
na / is_na Supported Generic na<T>() and is_na(...) for double / integer / bool, plus null-ID detection for PineMap<K,V>.
Logging / runtime errors Supported pine_log_info / warning / error, pine_runtime_error (throws).
Maps Supported within explicit codegen boundaries map.hpp provides ordered PineMap<K,V> handles, Pine alias/copy/null semantics, typed missing values, the 50,000-pair limit, and primitive-only rollback snapshots. Generated strategies use this runtime for supported string-key/primitive-value maps; unsupported map history, nested map-bearing matrices, and ambiguous specializations fail closed.
Arrays / UDTs No runtime module (Pine surface still supported via consumer compiler) Pine arrays and UDTs work through transpiler-emitted std::vector<T> and generated C++ structs. Recursive snapshotting for UDTs or collections containing map/reference handles remains a codegen/type-system responsibility.
Drawing / plotting / alerts Drawings as data; no plotting or alert module drawing.hpp keeps line / box / label / linefill / chart.point geometry as data that trading logic reads back; visual setters are accepted no-ops. Plots, tables, polylines and alerts: PineForge's transpiler parses-and-skips these so the strategy still compiles and runs, but no visual side-effects are emitted.

Public C ABI

<pineforge/pineforge.h> is the single canonical consumer header. It has exactly 94 public PF_API declarations: 74 of the 80 runtime implementations (the other six are declared in execution_observer.h and selected_window.h) and twenty per-strategy generated exports. Every compiled PineForge strategy .so exports that public set, except that strategy_declares_bar_magnifier is exported only by a script that declares use_bar_magnifier = true and the three outputs exports only by a module that records outputs; older modules lack the six opt-in checked-settings exports and the two opt-in execution-capability exports. The historical 28-symbol module sentence was not a current module inventory; the grouped table below is a guide, not the count:

Symbol Role
strategy_create Allocate a strategy instance
strategy_free Release the instance
run_backtest Run with auto-detected timeframe
run_backtest_full Run with timeframe + magnifier configuration
report_free Free arrays inside a filled pf_report_t
strategy_closed_trade_entry_incarnation Read per-run physical entry provenance
strategy_set_input Override a Pine input.*() value
strategy_set_override Override a strategy(...) declaration param
strategy_set_magnifier_volume_weighted Toggle volume-weighted magnifier
strategy_set_trace_enabled Toggle per-bar trace recording
strategy_set_trade_start_time Earliest Unix-ms at which order commands may fire
strategy_stream_begin Warm on confirmed OHLCV and enter realtime mode
strategy_stream_push_tick Push one normalized ordered trade
strategy_stream_push_ticks Push one contiguous ordered-trade array
strategy_stream_advance_time Confirm elapsed bars and materialize quiet intervals
strategy_stream_end End the realtime lifecycle
strategy_stream_fill_report Snapshot cumulative warmup + realtime state
strategy_set_chart_timezone Chart display TZ (intraday-day rollover gates)
strategy_set_syminfo_timezone Exchange TZ (syminfo.timezone)
strategy_set_syminfo_session Trading session string (syminfo.session)
strategy_set_syminfo_mintick Instrument tick size (syminfo.mintick)
strategy_set_syminfo_pointvalue Futures $-per-point multiplier (syminfo.pointvalue)
strategy_set_syminfo_metadata Inject fundamental / exchange metadata by Pine member name
strategy_set_account_currency_fx_series Effective-time quote-to-account conversion curve
strategy_configure_native_fx_curve_v1 Stage or clear an immutable FX curve on a Ready native handle
strategy_get_last_error Error message from the most recent failed run
pf_version_get Runtime version (struct)
pf_abi_version Caller-allocated POD layout version
pf_version_string Runtime version (string)

POD types (pf_bar_t, pf_trade_tick_t, pf_trade_t, pf_report_t, the metrics/equity structs, pf_security_diag_t, pf_trace_entry_t, pf_version_t) and the pf_magnifier_distribution_t enum complete the surface. Stability: from 1.0.0, the public contract states the rules: within the same PINEFORGE_VERSION_MAJOR, struct layouts and extern "C" signatures are append-only. Before 1.0 they were not (pf_report_t grew and two pf_equity_stats_t fields were renamed after v0.13.1; see CHANGELOG.md). New fields may be appended; existing fields are never reordered, removed, or retyped. New functions may be added; existing functions are never removed or signature-changed. Compile-time static_asserts in src/c_abi.cpp pin the layouts against drift.

The C++ headers generated strategies compile against (<pineforge/engine.hpp>, <pineforge/ta.hpp>, the Pine source layer under include/pineforge/source/) are outside the version guarantee; the codegen pairing rule covers them. The native C++ API (<pineforge/native_host.hpp> and the headers the public contract lists) is a public surface.

Runtime modules — file layout

Headers live under include/pineforge/ and implementations under src/. Several large concerns are split across multiple .cpp files (declarations stay in the matching single .hpp):

Module Header Source Pine-facing role
Public C ABI pineforge.h c_abi.cpp (+ layout static_asserts) 94 public PF_API declarations: 74 of the 80 runtime implementations plus twenty per-strategy generated exports. strategy_configure_native_fx_curve_v1 stages the additive native FX curve.
Engine engine.hpp engine_run.cpp, engine_stream.cpp, engine_execution.cpp, engine_orders.cpp, engine_path_resolve.cpp, engine_trade_accessors.cpp, engine_security.cpp, engine_lower_tf.cpp, engine_report.cpp, native_execution_consumer.cpp One-shot and continuous lifecycle, native request matching/settlement, orders, reports, inputs / syminfo, magnifier, TF aggregation, and request.security plumbing.
Engine internals engine_internal.hpp (private cross-TU header) pineforge::internal::* types and helpers shared between engine .cpp partitions; not part of the public ABI.
Technical analysis ta.hpp ta_moving_averages.cpp, ta_oscillators.cpp, ta_volatility_trend.cpp, ta_extremes_volume.cpp, ta_misc.cpp Official ta.* functions and series variables backed by stateful runtime classes with compute / recompute, plus pivot_point_levels(...) free function.
Math math.hpp math.cpp Inline pine_random(...) PRNG and rolling math::Sum class.
Strings str_utils.hpp str_utils.cpp Format, format-time, regex match, split, and numeric-to-string helpers.
Timeframe timeframe.hpp timeframe.cpp TF string parsing, ratio computation, calendar detection, TimeframeAggregator.
Session / time session_time.hpp session_time.cpp pine_time(...), pine_time_close(...) with session and timezone gating.
Timezone (private) timezone.hpp timezone.cpp tz_util::ScopedTimezone — mutex-guarded TZ env-var swap for thread-safe formatting. Internal; not in public include path.
Bar magnifier magnifier.hpp magnifier.cpp OHLC price-path sampling with six distribution modes; optional volume-weighted sample density.
Matrices matrix.hpp matrix.cpp Eigen-backed PineMatrix.
Generic matrices generic_matrix.hpp header-only Template PineGenericMatrix<T> over std::vector<std::vector<T>> (T=bool specialized to vector<vector<char>>) for non-double element types.
Maps map.hpp header-only PineMap<K,V> handle runtime with insertion ordering, Pine-aware primitive keys, null IDs, 50,000-pair cap, explicit container copy, and primitive-value snapshot/restore. The transpiler emits this runtime for its supported map boundary.
Drawings drawing.hpp header-only line / box / label / linefill handles in per-type arenas, and chart.point: geometry as data, no rendering.
Series history series.hpp header-only Generic Series<T> deque with push / update / [k] indexing.
na na.hpp, map.hpp header-only na<T>() generators and is_na(...) checks, including the null-ID overload for PineMap<K,V>.
Bar struct bar.hpp header-only struct Bar { double open, high, low, close, volume; int64_t timestamp; }; (Unix milliseconds).
Color color.hpp header-only 17 named ARGB constants plus new_color, r, g, b, t.
Logging log.hpp header-only pine_log_info / warning / error (stderr) and pine_runtime_error (throws std::runtime_error).

Engine lifecycle

BacktestEngine is an abstract base; the consumer compiler emits a strategy class that derives from source::PineStrategyHost (a NativeStrategyHost, itself a BacktestEngine) and implements on_bar(const Bar&). Three run(...) overloads are exposed:

void run(const Bar* bars, int n);

void run(const Bar* input_bars, int n_input,
         const std::string& input_tf,
         const std::string& script_tf,
         bool bar_magnifier = false,
         int magnifier_samples = 4,
         MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS);

void run(const Bar* input_bars, int n_input,
         const std::string& input_tf,
         const std::string& script_tf,
         const std::unordered_map<std::string, std::string>& inputs,
         const SymInfo& syminfo,
         const void* overrides = nullptr,   // opaque; a source::StrategyOverrides* here
         bool bar_magnifier = false,
         int magnifier_samples = 4,
         MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS);

The TF-aware overload auto-detects input_tf from bar timestamps when empty (via detect_timeframe) and defaults script_tf to input_tf. The full overload additionally injects SymInfo, the input map, and a StrategyOverrides struct (NaN / -1 mean "leave default").

StrategyOverrides only carries a fixed set of override fields: initial_capital, commission_value, default_qty_value, pyramiding, slippage, commission_type, default_qty_type, process_orders_on_close, calc_on_order_fills, close_entries_rule (source/pine_adapter.hpp). Anything else (currency, margin, risk thresholds, etc.) must be set by the generated subclass — there is no runtime entry point for it.

Per-input runtime overrides are written via set_input(key, value) / clear_inputs() on BacktestEngine before run(...). Magnifier sample density can be flipped to volume-weighted via set_magnifier_volume_weighted(bool).

Strategy orders and state

Order entry points

These are members of source::PineStrategyHost (include/pineforge/source/pine_strategy_host.hpp).

Method Notes
strategy_entry(id, is_long, limit, stop, qty, comment, oca_name, oca_type, qty_type) Replaces an existing pending order with the same id. Plain market entry under process_orders_on_close=true fills immediately at bar close so position_avg_price is correct for follow-up strategy_exit calls.
strategy_order(id, is_long, qty, limit, stop, oca_name, oca_type) "Raw" pending order. When direction opposes the open position the order is treated as exit-style for fill resolution.
strategy_exit(id, from_entry, limit, stop, trail_points, trail_offset, trail_price, qty_percent, comment) Reserves a slice of the open position; partial exits with the same id are one-shot per live position.
strategy_close(id, comment, qty, qty_percent, immediately) FIFO close by entry id (or all when id is empty). Honours PineStrategyConfig::close_entries_rule_any for ANY-mode partial close. immediately bypasses pending-order resolution.
strategy_close_all() Convenience wrapper for strategy_close("").
strategy_cancel(id) / strategy_cancel_all() Drops pending orders by id or globally.

Native resting requests are resolved at each native driver decision point, which walks a 4-waypoint OHLC path (O → H → L → C or O → L → H → C depending on open proximity to high vs low). The runtime resolves stop / limit priority, gap fills, opposing-stop arbitration, OCA siblings, and trail levels along that path. slippage_ (in ticks) and syminfo_mintick_ round all fill prices; stop entries use directional mintick snapping (long stops up, short stops down) to match TradingView.

Priced strategy.entry orders also track TradingView's deferred-flip carry rule. When an opposite priced entry is placed while a position is open, then fires later from flat after a strategy.close / strategy.close_all, the runtime opens qty + carried_position_qty. Source order inside a single on_bar(...) matters: close calls that appear before the entry reduce the captured carry for that entry.

strategy_exit accepts price params (profit, loss, limit, stop, trail_*); the runtime's exit method itself does not enforce that at least one is set — that policy lives outside the runtime.

Position-sizing and commission

Quantity sizing is governed by PineStrategyConfig::default_qty_type (enum QtyType { FIXED, PERCENT_OF_EQUITY, CASH }) and default_qty_value. A Pine v6 script that omits initial_capital, default_qty_type or default_qty_value runs with 100000, strategy.percent_of_equity and 100, which pineforge-codegen 1.0.0 declares in the generated constructor; code generated by an earlier codegen and a hand-built PineStrategyConfig keep 1000000, strategy.fixed and 1. Commission is commission_type_ (enum CommissionType { PERCENT, CASH_PER_ORDER, CASH_PER_CONTRACT }) and commission_value_. Both are per-trade; there is no separate runtime entry point for strategy.default_entry_qty.

Margin uses PineStrategyConfig::margin_long / margin_short percentages (100 = no leverage). The Pine adapter admits an opening by TradingView's money rule and books TradingView's margin calls through a maintenance-only NativeMarginModel (see PineScript to native C++).

Risk

The generated strategy declares the six strategy.risk.* limits through PineStrategyHost::set_pine_risk_direction, set_pine_risk_max_position_size, set_pine_risk_max_drawdown, set_pine_risk_max_intraday_loss, set_pine_risk_max_cons_loss_days and set_pine_risk_max_intraday_filled_orders; the Pine adapter enforces them (PineExecutionAdapter::update_risk_state):

Pine limit Effect
strategy.risk.allow_entry_in Block entries against the allowed direction.
strategy.risk.max_position_size Block new entries when current position_qty_ ≥ cap.
strategy.risk.max_drawdown Halt strategy when peak-to-trough drawdown crosses the cap (absolute $ or % of peak equity).
strategy.risk.max_intraday_loss Halt strategy when running intraday P&L crosses the cap. Day boundary uses month / day-of-month, not session.
strategy.risk.max_cons_loss_days Halt strategy after N consecutive losing days.
strategy.risk.max_intraday_filled_orders Latch-till-day-rollover fill cap: the cap-triggering fill emits TV's synthetic cap-close, then all further fills (and order placement) on that chart-day are dropped. The chart-day key and the quota rules are compat::pine::IntradayCap's (include/pineforge/compat/pine/intraday_cap.hpp).

The drawdown and consecutive-loss-day halts are one-way: once either latches, no new entries are accepted for the remainder of the run. None of these limits is a StrategyOverrides key; the generated strategy sets them.

Trade accessors

strategy.closedtrades.* accessors are wired (defined inline on BacktestEngine):

profit, profit_percent, commission,
entry_bar_index, exit_bar_index,
entry_comment, exit_comment, entry_id, exit_id,
entry_price, exit_price, entry_time, exit_time,
size, max_runup, max_runup_percent, max_drawdown, max_drawdown_percent

strategy.opentrades.* accessors mirror the closed set minus the four exit_* fields (exits do not exist for an open trade):

profit, profit_percent, commission,
entry_bar_index, entry_comment, entry_id, entry_price, entry_time,
size, max_runup, max_runup_percent, max_drawdown, max_drawdown_percent

Pine v6 has no strategy.closedtrades.direction(...) / strategy.opentrades.direction(...) accessor — direction is encoded in the sign of size (positive = long, negative = short), and the support checker rejects any user code that calls a direction(...) accessor.

Aggregate strategy state methods are also defined on the engine: net_profit / gross_profit / gross_loss (and _percent variants), avg_trade / avg_winning_trade / avg_losing_trade (and _percent), count_wintrades / count_losstrades, current_equity, open_profit(price), open_trades_capital_held, and signed_position_size. The Pine source host additionally exposes margin_liquidation_price() as a source-level projection; it is not a generic BacktestEngine contract.

Bar metadata helpers

BacktestEngine::_decompose_bar_time() decomposes current_bar_.timestamp (UTC) into { year, month, dayofmonth, hour, minute, second, dayofweek, weekofyear } and individual scalar accessors (_bar_year(), _bar_hour(), …) are exposed for the consumer to read. A Bar stores a single int64_t timestamp; the Pine host's time_close() answers the chart bar's close from the session calendar (pine_time_close on syminfo.session and syminfo.timezone), and inside another symbol's request payload the feed's own close.

barstate flags the runtime tracks (barstate_islast_ on the engine, the tick flags on the Pine host's language state):

  • is_first_tick_ / is_last_tick_ — the Pine host's tick flags (PineLanguageState): a script bar's calculation runs at its terminal sub-bar (every bar without the magnifier) with both set.
  • barstate_islast_ — last script bar in the run.

Pine v6 exposes seven barstate.* flags; generated code reads barstate.ishistory and barstate.isrealtime as the constants true and false, in a stream too. The consumer compiler maps them onto the three engine flags above with the following batch-mode approximations:

Pine v6 flag PineForge batch-mode value
barstate.isfirst bar_index == 0 (handled by the consumer compiler).
barstate.islast barstate_islast_: true on the run's final bar.
barstate.ishistory always true (every bar is historical in batch mode).
barstate.isrealtime always false.
barstate.isnew follows is_first_tick_ (first sample of a script bar).
barstate.isconfirmed follows is_last_tick_ (last sample of a script bar).
barstate.islastconfirmedhistory barstate_islast_ (codegen warns it approximates).

calc_on_order_fills is modelled: the Pine adapter recalculates the script after each fill, with TradingView's script-state rollback. Live-tick semantics (calc_on_every_tick, which codegen does not read, and barstate.isnew flipping mid-bar on a live feed) are not modelled; see "varip and realtime tick semantics" below.

Technical Analysis (namespace ta)

Every TA class exposes both compute(...) (advance state, push history) and recompute(...) (re-run on the same bar — used by the magnifier and security intrabar paths so a TA's permanent state is not disturbed). State is owned per instance; the consumer compiler allocates one instance per call site.

A length that is neither a constant nor an input is the source layer's (include/pineforge/source/pine_ta_length.hpp): a simple length, fixed for the run, builds the class from the call site's first execution (FirstCallBound); a series length re-windows ta.highest, ta.lowest, ta.highestbars and ta.lowestbars at every call (SeriesHighest, SeriesLowest, SeriesHighestBars, SeriesLowestBars); ta.supertrend keeps its first execution's factor (PineSupertrend); a length of 0, a negative length or na stops the run. pineforge-codegen 1.0.0 lowers such calls (0.10.4 refuses them).

Tuple-returning TA classes

Class Result struct Fields
MACD MACDResult macd_line, signal_line, histogram
BB BBResult middle, upper, lower
KC KCResult middle, upper, lower
Supertrend SupertrendResult value, direction
DMI DMIResult diplus, diminus, adx

Stoch::compute(src, high, low) returns Pine v6's official single stochastic value. %K / %D smoothing is explicit Pine code, e.g. assign the result to k and compute d = ta.sma(k, length).

Single-value TA classes

Moving averages and smoothing (src/ta_moving_averages.cpp): SMA, EMA, RMA, WMA, HMA, VWMA, ALMA(length, offset=0.85, sigma=6.0, floor=false), SWMA (period-4 symmetric weights). ALMA's floor centres the Gaussian at floor(offset * (length - 1)) instead of offset * (length - 1), Pine's ta.alma(..., floor); omitting it is the unfloored ALMA, value for value (PF_ALMA_HAS_FLOOR, tests/test_ta_alma_floor.cpp).

Oscillators / momentum (src/ta_oscillators.cpp): RSI, Stoch, CCI, MFI, Mom, ROC, CMO, TSI(short_length, long_length), WPR, COG, RCI; (src/ta_volatility_trend.cpp): TR, ATR.

Bands / channels / widths (src/ta_volatility_trend.cpp): BB, KC, BBW, KCW. KC's middle band is the EMA of its source on every bar, ta::EMA's value included on the first bar; its range is the true range against the previous bar's close (na where that close is, as ta.tr is), so the upper and lower bands, and KCW, are na until the range EMA has a value (tests/test_ta_kc_basis.cpp). KC(length, mult, use_true_range = true) and KCW(length, mult, use_true_range = true) take Pine's useTrueRange: false averages high - low instead, which has a value on the first bar (PF_KC_HAS_USE_TRUE_RANGE, tests/test_ta_kc_range.cpp).

Trend / pivots (src/ta_volatility_trend.cpp): Supertrend(factor, atr_period), DMI(di_length, adx_smoothing), SAR(start, increment, maximum); (src/ta_extremes_volume.cpp): PivotHigh(left, right), PivotLow(left, right).

Cross / state machines (src/ta_oscillators.cpp): Crossover, Crossunder, Cross, Change(max_length=1), Rising(length), Falling(length); (src/ta_misc.cpp): BarsSince, ValueWhen(max_occurrence=1). Change::compute takes a double src; Pine v6's ta.change also accepts a bool source (returns true on flip). The consumer compiler is responsible for casting bool → 0.0 / 1.0 before feeding the runtime, since the runtime class itself is numeric only.

Statistical / windowed — split across three files: (src/ta_volatility_trend.cpp): StdDev, Variance, Dev (mean absolute deviation); (src/ta_extremes_volume.cpp): Median, Mode, Range, Highest, Lowest, HighestBars, LowestBars; (src/ta_misc.cpp): PercentRank, PercentileNearestRank, PercentileLinearInterpolation, Correlation.

Volume indicators (src/ta_extremes_volume.cpp): official ta.vwap(...) is a function backed by VWAP, in both forms. The single-value form maps to VWAP::compute / recompute; Pine v6's 3-tuple form [vwap, upper_band, lower_band] = ta.vwap(source, anchor, stdev_mult) is backed by the VWAPBands wrapper class (ta.hpp), which routes the standard compute / recompute dispatch to VWAP::compute_bands / recompute_bands with the construction-time stdev_mult (see tests/test_vwap_bands.cpp). VWAP restarts its accumulation when the symbol's session day changes, which is Pine's default anchor. Any other anchor is AnchoredVWAP: compute(src, volume, anchor) restarts the sums on every bar whose anchor is true (that bar is the first of the new accumulation) and answers na until the first such bar; compute_bands(src, volume, anchor, stdev_mult) is the 3-tuple form with a per-bar multiplier, and AnchoredVWAPBands(stdev_mult) wraps it for the standard compute / recompute dispatch. The arithmetic is VWAP's, so an anchor on exactly VWAP's reset bars (and on the first bar) reproduces it bit for bit; PF_VWAP_HAS_ANCHOR_INPUT marks the forms' presence (see tests/test_ta_anchored_vwap.cpp). Official ta.obv, ta.accdist, ta.nvi, ta.pvi, ta.pvt, ta.wad, ta.wvad, and ta.iii are series variables backed by OBV, AccDist, NVI, PVI, PVT, WAD, WVAD, and III. Parenthesized call forms such as ta.obv() are rejected by PineForge's support checker because they are not Pine v6 functions.

Cumulative / chart-extreme (src/ta_extremes_volume.cpp): Cum, AllTimeMax, AllTimeMin.

Linear regression (src/ta_misc.cpp): Linreg(length).compute(src, offset).

ta::TR first-bar behaviour

TR(bool handle_na=false).compute(high, low, close) matches Pine v6's ta.tr(handle_na) split. With the default handle_na=false, the first bar returns na; with handle_na=true, the first bar falls back to high - low. The property form ta.tr maps to the default form.

pivot_point_levels(method, high, low, close)

Free function in namespace ta. The runtime returns Pine v6's documented 11-slot order: P, R1, S1, R2, S2, R3, S3, R4, S4, R5, S5. Levels absent from the selected method are na<double>(). Current runtime inputs are method, high, low, close; the official Pine anchor / developing parameters are handled by the consumer compiler layer when present. Traditional, Fibonacci, Classic and Camarilla are PivotPointLevels' formulas bit for bit. Woodie and DM keep a historical approximation for source compatibility, because this signature carries neither the next period's open that Woodie's pivot weights (it weights the close) nor the period's open that DM compares with the close (it branches on the close meeting the high or the low). The overload pivot_point_levels(method, open, high, low, close, next_open) takes both and computes every type exactly as PivotPointLevels does (R5 lane B-ENGINE).

PivotPointLevels — pivot levels of an anchored period

PivotPointLevels::compute(type, anchor, developing, open, high, low, close) (and recompute, with the same arguments) is the stateful form of Pine's ta.pivot_point_levels(type, anchor, developing). type is a PivotLevelsType or one of the six names (pivot_levels_type(name), which refuses any other name with std::invalid_argument). A period runs from an anchored bar (bar 0 before the first anchor) to the bar before the next anchored bar, aggregated as its first open, highest high, lowest low and last close (finite values only). With developing = false it answers the levels computed on the last anchored bar from the period that bar closed -- Woodie with that bar's own open, the open of the period the levels are for -- and holds them until the next anchored bar (na before the first). With developing = true it recomputes the levels of the period in progress on every bar; Woodie has no developing levels, and that pair throws std::runtime_error, which fails the run like any exception out of a script callback (strategy_get_last_error carries the text). The formulas are the standard definitions, operation by operation (Traditional R3 = P * 2 + (H - 2 * L), ..., S5 = P * 4 - (4 * H - L); Woodie P = (H + L + 2 * open) / 4, R3 = H + 2 * (P - L), R4 = R3 + (H - L); DM's X on the period's open against its close). Anchored on every bar with developing = false it equals the six-argument free function above for every level of every type, and the four-argument one for every type but Woodie and DM (PF_PIVOT_LEVELS_HAS_ANCHOR, tests/test_ta_pivot_point_levels.cpp).

Math (<pineforge/math.hpp>)

The runtime exposes only two pieces under math:

Symbol Signature Notes
pine_random(lo, call_site, hi, seed, bar_index) inline free function Deterministic SplitMix64-style mixer. Stable across platforms / runs; not TradingView's PRNG.
math::Sum(length) class with compute(src) / recompute(src) Rolling sum used to back PineScript math.sum(source, length). na sources are ignored: output stays na until length non-na values exist, then retains the sum of the last length non-na values, including on na-input bars.

Order-fill rounding to mintick lives on BacktestEngine::round_to_mintick(price). Every other Pine math function is the consumer compiler's responsibility — the runtime intentionally provides no abs, sqrt, trig, min / max, etc. PineForge's transpiler emits those inline against <cmath>.

Strings (<pineforge/str_utils.hpp>)

Helper Signature Behaviour
pine_str_format (fmt, vector<string> args) MessageFormat over text arguments: {N} and {N,number,<style>} insert args[N] as it is; text between single quotes is literal and '' is one quote; a placeholder with no such argument, or whose index is not a number, is kept as written.
str_format_values (fmt, vector<StrFormatValue> args) The same, with number arguments: {N} renders a number as #,###.###, and {N,number,<style>} as its style (integer, percent ×100, currency, or a decimal pattern). Text and bool arguments are inserted as text.
pine_str_format_time (timestamp_ms, format, timezone) Maps Pine tokens (yyyy / MM / dd / HH / mm / ss) to strftime and formats. Empty / "UTC" / "Etc/UTC" use gmtime_r; everything else swaps TZ under tz_util::ScopedTimezone and uses localtime_r.
pine_str_match (source, regex_pattern) Returns the first capture group if any, else the full match. Empty string on no match or regex error.
pine_str_split (source, separator) Returns vector<string>. Empty separator yields {source}.
pine_str_tostring (value, format_mode = "", mintick = 0) The value's shortest round-trip decimal digits, rounded half-up on those digits. NaN, Infinity, -Infinity; no sign on a value that rounds to zero. Modes: default (up to ten fraction digits), "percent" (up to two, then %; the value is not scaled), "volume" (K / M / B / T with up to two fraction digits; below a thousand, no fraction digits), "mintick" (rounds to mintick, decimal places implied by mintick: the times the tick is multiplied by ten to reach 1, so a 0.25 tick prints one decimal and 0.3 renders "0.2", the rendering generated code delegates to and kept as it was; without a positive tick it is the default mode), or a decimal pattern (#.##, #.00, #,###, #.##%).

Enum-string lookup for str.tostring(<enum_member>) is implemented by pine_enum_str_at(table, n, idx) (defined in source/pine_policy_support.hpp), which clamps the index to the table size to avoid out-of-bounds reads.

Other string operations (length, contains, replace, etc.) are not part of the runtime API — the consumer compiler emits those inline.

Inputs

Inputs are stored as std::unordered_map<std::string, std::string> on the engine. Generated strategy code reads them through typed getters that fall back to the Pine default on missing key or parse failure:

double  get_input_double(const std::string& key, double  default_val) const;
int     get_input_int   (const std::string& key, int     default_val) const;
int64_t get_input_int64 (const std::string& key, int64_t default_val) const;
bool    get_input_bool  (const std::string& key, bool    default_val) const;
std::string get_input_string(const std::string& key, const std::string& default_val) const;
const Series<double>& get_input_source(const std::string& key,
                                       const Series<double>& default_series) const;

get_input_bool accepts "true" / "1" and "false" / "0" (anything else returns the default). get_input_double / _int / _int64 route through std::stod / std::stoi / std::stoll with a try / catch around parse errors. get_input_int64 backs 64-bit input payloads such as input.color (packed ARGB). get_input_source backs input.source runtime overrides: it resolves a native source name ("open", "high", "low", "close", "volume", "hl2", "hlc3", "ohlc4", "hlcc4") to the engine's always-materialized source-series history, falling back to the codegen-resolved default series when the key is absent or the override is non-native.

The runtime is intentionally agnostic about the kind of input (input.float / .int / .bool / .string / .source / .color / .timeframe / …); all inputs are presented as strings and the typed getter at the call site decides the parse. UI metadata (group, inline, tooltip, display, confirm, options, min / max / step) has no runtime backing.

request.security()

The runtime owns same-symbol security computation, and since lane XSYM-D the merge of another symbol's installed bars (below). Per-call state lives in SecurityEvalState:

struct SecurityEvalState {
    int sec_id;
    std::string tf;
    TimeframeAggregator aggregator;
    Bar current_bar;
    bool gaps_on, lookahead_on;
    bool lower_tf_requested, lower_tf_emulation;
    int lower_tf_ratio, lower_tf_seconds;
    int current_sub_bar_count;
    int64_t feed_count, eval_complete_count, eval_partial_count;
    bool lower_tf_array_requested;
    int lower_tf_sub_bar_index;
};

Lifecycle hooks the generated subclass implements:

  • configure_security_evaluators() — called once at the start of run(...); the subclass calls register_security_eval(sec_id, requested_tf, input_tf, lookahead_on, gaps_on) for each request.security() call site.
  • evaluate_security(sec_id, bar, is_complete) — invoked by the runtime each time a security bar is ready (complete or partial under lookahead_on).
  • clear_security(sec_id) — invoked when gaps_on produces an empty bar.

For request.security_lower_tf(...), the generated subclass registers with register_security_lower_tf_eval(sec_id, requested_tf, input_tf) and reads security_lower_tf_sub_bar_index(sec_id) during synthesis so it can clear and append to the returned array in earliest-to-latest order.

Per-bar feed semantics (feed_security_eval_state):

  • Higher-TF requests route input bars through TimeframeAggregator.
    • On a complete aggregated bar: eval_complete_count++, then evaluate_security(...) with is_complete=true.
    • On a partial bar with lookahead_on: eval_partial_count++, then evaluate_security(...) with is_complete=false.
    • On a partial bar with gaps_on: clear_security(sec_id).
    • Otherwise the partial bar is silently held until completion.
  • Lower-TF emulation (lower_tf_emulation=true) synthesizes intrabar bars from the input bar via synthesize_lower_tf_bars, which samples the OHLC path in ratio + 1 ENDPOINTS-distribution points, time-stamps each slice on a fixed requested_seconds grid, and divides volume evenly (with the remainder on the last slice). Each synthetic bar is fed as a complete update.

supports_lower_tf_emulation only accepts emulation when both input and requested timeframes are fixed intraday minute strings (no D / W / M / S suffix), requested < input, and input_seconds % requested_seconds == 0.

ensure_supported_lower_tf_emulation_flags rejects lower-TF emulation when lookahead_on or gaps_on is set — emulation is lookahead_off / gaps_off only.

Another symbol. A site of another symbol registers with register_security_eval(sec_id, symbol, requested_tf, input_tf, lookahead_on, gaps_on, ignore_invalid_symbol) (PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1) and reads that symbol's own bars, installed before the run through strategy_set_symbol_feed / _feed_column / strategy_set_symbol_facts. The kernel hands the site every bar of that feed that has closed by the chart bar's close (lookahead_off) or opened by its open (lookahead_on), in order, each with the close the feed gives it (NativeRunSpec::instrument_feeds); the source host runs the payload on each in the requested context -- its own history, bar_index, time_close and syminfo.* -- and fails the run closed, naming the symbol and the timeframe, when no feed is installed. On a D, W or M chart the source host judges the merge against the chart bar's own time and time_close, as TradingView does, where the kernel reads a daily label stamped in its session's break (OANDA:XAUUSD at 17:00 ET) as the session that closes at it (tests/test_foreign_break_stamp_tapes.cpp). pineforge-codegen 1.0.0 lowers a site of another symbol onto this surface (lane XSYM-E; 0.10.4 refuses it); see Native engine, "Instrument feeds: another symbol's bars".

request.security_lower_tf(...) is supported for same-symbol lower timeframes that satisfy the same emulation constraints. It returns an array whose elements are ordered earliest-to-latest within the chart bar, matching Pine v6's documented return shape. PineForge currently supports numeric and bool element arrays; tuple, UDT, color, and string element arrays are rejected by the transpiler.

validate_security_timeframes(input_tf) runs at the start of run(...) and throws when:

  • a request exists but input_tf is empty, or
  • a request.security_lower_tf TF is finer than the input but does not satisfy the lower-TF emulation constraints above, or a request.security TF is finer than a chart fed only its own bars. A run given bars finer than the chart reads na on every bar from a request.security finer than all of them, as TradingView reads a timeframe it holds no bars of.

Beyond that, the runtime checks the symbol argument only for another symbol's site (no installed feed fails the run, naming the symbol and the timeframe; ignore_invalid_symbol reads na) and rejects no other request.* variant — those rejections live in the surrounding compiler layers.

Bar magnifier

With a feed finer than the chart, the bar magnifier walks TradingView's own intrabars at TradingView's intrabar timeframe (a 15-minute chart walks 2-minute bars), each owned by the chart bar holding its last minute (tradingview_magnifier_bars, source/magnifier_intrabars.hpp). With the chart's own bars only, it samples each bar's OHLC path.

MagnifierDistribution has six modes (in magnifier.hpp):

Mode Sample placement
UNIFORM Equal arc-length spacing along the OHLC path.
COSINE Chebyshev-like density at segment endpoints.
TRIANGLE Density at midpoints of each segment.
ENDPOINTS (default) Always include exact O, H, L, C with uniform fill in between.
FRONT_LOADED Density near O.
BACK_LOADED Density near C.

sample_price_path(bar, n, dist) samples at least 2 points and always emits exactly O first and C last. The middle leg sequence is O → H → L → C when the open is closer to high, otherwise O → L → H → C (ties go low-first).

sample_price_path_volume_weighted(bar, base, mean_volume, min=2, max=64, dist) scales sample count by bar.volume / mean_volume, clamped to [min, max]. The native execution consumer precomputes the per-bar mean volume so each sub-bar's tick density is relative to its own script bar's average. The toggle is set_magnifier_volume_weighted(bool).

The kernel matches orders over every sub-bar of a magnified script bar; the Pine host runs the script once, at the terminal sub-bar (is_first_tick_ and is_last_tick_ are set there), so generated on_bar(...) advances series history exactly once per script bar.

Series, Bar, and na

Series<T> (header-only template in series.hpp):

template<typename T> class Series {
    void push(T value);     // new bar — newest at the front
    void update(T value);   // overwrite current bar (magnifier intrabar)
    T operator[](int k) const;  // 0 = current, k >= 1 = k bars ago, out-of-range -> na<T>()
    T current() const; int size() const; void clear();
};

max_len defaults to 500; out-of-range or negative offsets return na<T>().

Bar:

struct Bar { double open, high, low, close, volume; int64_t timestamp; };

timestamp is Unix milliseconds. There is no separate close timestamp at the storage layer.

na:

template<typename T> T na();         // double -> NaN, int -> INT_MIN, int64_t -> INT64_MIN, bool -> false
inline bool is_na(double v);          // std::isnan
template<typename T, ...> bool is_na(T v);  // integer overload (== the type's minimum)

Color

pine_color::* holds 17 named ARGB constants. Helpers:

  • new_color(c, transp) — set the alpha byte to the whole number nearest 255 × (100 − transp) / 100, clamped to 0–255 (an na transparency is fully transparent).
  • r(c), g(c), b(c) — channel bytes.
  • t(c) — recover transp (0–100) from the alpha byte.

Drawing objects are data in drawing.hpp (see the summary table); the runtime has no charting or rendering types.

Timeframes

tf_to_seconds(tf) covers minute strings ("1", "5", "60", "240", …), day strings ("D", "1D" → 86400), and week strings ("W", "1W" → 604800). Month ("M", "1M") returns -1 to flag calendar mode. tf_multiplier, tf_is_intraday / _daily / _weekly / _monthly / _seconds are inline string predicates.

tf_change(prev_ms, curr_ms, tf) and crosses_boundary(prev_ms, curr_ms, period) provide TF / calendar boundary detection.

tf_ratio(input_tf, target_tf) returns:

  • > 1 for ratio aggregation,
  • 1 for same TF,
  • -1 for calendar aggregation (month),
  • -2 when the target TF is finer than the input.

detect_timeframe(bars, n, max_samples=100) infers a TV-style TF string from median timestamp deltas, with fallback "1" on insufficient or irregular data.

TimeframeAggregator runs in three modes:

  • PASSTHROUGH (default constructor),
  • RATIO (constructor (int ratio) — every ratio input bars produce one output bar),
  • CALENDAR (constructor (target_tf, input_tf) — aggregate to day / week / month boundaries).

feed(bar) returns an AggregatedBar { Bar bar; bool is_complete; int sub_bar_count; }.

Time / Session / Timezone

pine_time(bar_ms, tf, session, tz, chart_tf) and pine_time_close(...) return Unix milliseconds, or na<int64_t>() when no bar of tf built on the requested session holds the bar (TradingView semantics for filtered sessions). They handle session string parsing and timezone conversion internally. A session argument builds its own bars, in its timezone (the explicit one, else syminfo.timezone): a D bar runs from a session day's first window open to its last close, a W or M bar from the first session day of its week or month to the next one's, and an intraday bar opens at each window's open (tests/fixtures/session_period); the chart's own time_close on an intraday chart is that grid on syminfo.session. A close is the boundary itself: on an intraday chart a W or M closes where the next one opens, on a daily chart at its last traded close (tests/fixtures/time_close_function). A 24-hour session's day in a zone with daylight saving runs from one wall-clock open to the next, 23 or 25 hours across a switch (tests/fixtures/dst_day_close).

tz_util::ScopedTimezone(tz) is RAII — it grabs a process-wide mutex, swaps TZ (lazily: a same-zone request skips the setenv / tzset pair), and holds the mutex until the guard is destroyed, so the caller's localtime_r / mktime decomposition inside the scope runs under a stable TZ. This is the only reason pine_str_format_time and the session helpers are safe to call from a multi-strategy harness (see tests/test_timezone_concurrency.cpp).

Matrices

PineMatrix wraps Eigen::MatrixXd. Member surface:

Group Methods
Construction new_(rows, cols, init_val=0) (static)
Access get, set, fill, row, col, rows, columns
Row / column ops add_row(idx, values), add_col(idx, values), remove_row, remove_col, swap_rows, swap_columns
Transform copy, submatrix(from_row, to_row, from_col, to_col), reshape(rows, cols), reverse, transpose, sort(column, ascending=true), concat(other, horizontal)
Aggregation avg, min, max, mode, sum
Arithmetic diff, mult, pow(n)
Linear algebra det, inv, pinv, rank, trace, eigenvalues, eigenvectors
Kronecker kron(other)
Count elements_count
Predicates is_square, is_identity, is_diagonal, is_antidiagonal, is_symmetric, is_antisymmetric, is_triangular, is_stochastic, is_binary, is_zero

The element type is fixed to double. UDT-typed matrices are not runtime-supported (see "Not implemented anywhere" below).

Runtime diagnostics

fill_report(ReportC*) populates:

Field Meaning
total_trades, trades, trades_len, net_profit Closed trade summary; trades is heap-allocated TradeC[], freed via free_report.
input_bars_processed, script_bars_processed Bar counters from the run(...) loop.
magnifier_sub_bars_total, magnifier_sample_ticks_total Magnifier work counters (sub-bars consumed × ticks sampled).
input_tf_seconds, script_tf_seconds, script_tf_ratio, needs_aggregation, bar_magnifier_enabled TF / aggregation diagnostics for the run.
security_feeds_total, security_eval_complete_total, security_eval_partial_total Aggregate request.security counters.
security_diag, security_diag_len Per-security SecurityDiagC { sec_id, feed_count, eval_complete_count, eval_partial_count }.
trace, trace_len, trace_names, trace_names_len Optional per-bar trace records and interned trace-name table, populated only when tracing is enabled.

Each TradeC carries entry_time / exit_time / entry_price / exit_price / pnl / pnl_pct / is_long / max_runup / max_drawdown / qty / commission / entry_bar_index / exit_bar_index / open_at_end (where max_runup / max_drawdown are the whole trade's peak favorable / adverse excursions in account currency, net of entry fees). ReportC also carries metrics, equity_curve and broker_state_hash.

Logging and runtime errors

log.hpp exposes four inline functions:

inline void pine_log_info(const std::string& msg);     // stderr "[INFO] "
inline void pine_log_warning(const std::string& msg);  // stderr "[WARN] "
inline void pine_log_error(const std::string& msg);    // stderr "[ERROR] "
inline void pine_runtime_error(const std::string& msg); // throws std::runtime_error

The runtime itself raises std::runtime_error from validate_security_timeframes, feed_security_eval_state (lower-TF synthesis failure), and ensure_supported_lower_tf_emulation_flags.

What the runtime does not implement

The two lists below distinguish between PineForge does not support this at all and the runtime has no module for this, but PineForge supports it via the consumer compiler's emitted code.

Runtime present — consumer compiler owns conservative boundaries

  • Pine map<K,V> has a dedicated header-only PineMap<K,V> runtime in map.hpp. It implements map-ID aliasing, map.copy() container separation, insertion-ordered keys/values, typed missing results, null IDs, Pine-aware primitive keys, and the 50,000-pair limit. The transpiler now emits this runtime for supported string-key/primitive-value maps, including typed and inferred na, UDF/UDT parameter and return propagation, once-only receiver evaluation, pair iteration, and supported checkpoint alias/rebind behavior. Public runtime snapshots remain deliberately limited to primitive values; the transpiler rejects map-bearing history, nested map-bearing matrices, incompatible inferred specializations, and other ambiguous paths instead of silently emitting incorrect C++.

No runtime module — Pine surface still supported via consumer compiler

These features work in PineForge code today; the runtime simply does not own a dedicated class or function for them. PineForge's transpiler emits inline C++ against <cmath>, <vector>, and generated structs.

  • Pine array<T> — emitted as std::vector<T> by PineForge's transpiler.
  • User-defined types (UDTs) — emitted as plain C++ structs; nested fields and array<UDT> are also handled there.
  • Currency conversion (strategy.convert_to_*) — no runtime feed; PineForge's transpiler treats this as identity (no FX adjustment).
  • Most scalar math.* functions (abs, sqrt, min, max, trig, round, etc.) — PineForge emits these against <cmath> / inline expressions.
  • Most str.* operations (length, contains, replace, lower, upper, tonumber, etc.) — PineForge emits these against std::string.

Not implemented anywhere — gaps with a future story

These are not supported by PineForge as a whole today. Each item carries a forward-looking assessment using these buckets:

Tag Meaning
Easy Mechanically straightforward — small, localized runtime / consumer-compiler change, no architectural shift.
Feasible Doable with non-trivial work but no fundamental conflict with PineForge's offline-batch model.
Feasible — needs aux data Mechanically feasible, but requires the user to provide an external dataset PineForge does not currently ingest.
Out of scope by design Conflicts with PineForge's offline-batch / paid-parity-validity model. Not a roadmap item even if mechanically possible.
Out of scope structurally Cannot be done without a feature PineForge does not have (e.g. live data feed).

Drawing / charting / alerts

plot, plotshape, plotchar, plotcandle, plotbar, plotarrow, fill, hline, bgcolor, barcolor, table.*, polyline.*, the visual setters of label.* / line.* / box.* / linefill.* (their geometry is runtime data, drawing.hpp), alert(...), alertcondition(...).

  • Feasibility: Feasible for plotting primitives (capture series + style metadata into the ReportC extension or a side-channel CSV / JSON for an external renderer). Realtime alert(...) is now also feasible because the engine has an explicit realtime lifecycle, but it still needs alert frequency/dedup state, an event ABI, and delivery plumbing.
  • Future story: A "report-as-data" path is the obvious target — plot(...) and friends would write tagged time-series rows into a new diagnostics array on ReportC, and a Python harness would render them with Plotly / matplotlib. alertcondition(...) results could be returned as a list of (bar_time, message) triples. Alert delivery should drain deterministic engine events into an external JSON-only webhook adapter so network retries never mutate engine state.
  • Why not done yet: Backtests already produce TradeC[] and per-bar diagnostics; visual plotting has not been the unblocker for any user-facing strategy validation. It is a UX feature, not a correctness feature.

varip and realtime tick semantics

barstate.isrealtime, barstate.isnew in realtime and calc_on_every_tick on realtime ticks.

  • Feasibility: Feasible. Ordered trades reach the broker and calc_on_order_fills already recalculates with TradingView's rollback, but realtime barstate flags and per-tick recalculation still need codegen support.
  • Status of varip: pineforge-codegen 1.0.0 accepts varip (0.10.4 rejects it): a historical bar executes once, so a varip keeps its value like var, and it is left out of the calc_on_order_fills rollback.
  • Why not done yet: the realtime distinction TV makes is meaningful only when the data feed is live.

Import / export / library system

import <user>/<lib>/<version>, export keyword, library(...) declaration.

  • Status: pineforge-codegen 1.0.0 (0.10.4 refuses every import) inlines the libraries a strategy imports: transpile(source, libraries={...}) takes each library's source by its import path, only the exports the script reaches are inlined, and a v5 library keeps v5's rules. An import of a built-in namespace that names only built-ins is a no-op. A library(...) script itself, and export in a strategy, are still refused. The runtime needs no changes.

External request.* variants

request.financial(symbol, field, period), request.dividends, request.earnings, request.splits, request.currency_rate, request.economic(country_code, field, ...), request.seed(source, symbol, expression), request.quandl, request.footprint.

With request.footprint, these are the nine request.* calls Pine v6 exposes outside request.security and request.security_lower_tf. pineforge-codegen 0.10.4 rejects all nine at transpile. In pineforge-codegen 1.0.0 request.economic, request.seed, request.quandl and request.currency_rate are still rejected; request.financial, dividends, earnings, splits and footprint transpile with a warning, read na when their value reaches only plots and alerts, and otherwise read the recorded series below (a footprint: another symbol's feed column) or stop the run where the value is read.

  • Feasibility:
    • request.financial / dividends / earnings / splits: Feasible — the runtime half exists. The source host keeps recorded request series (strategy_set_recorded_series, lane XSYM-D): a request key maps each chart bar's open time to the value TradingView returned on it, recorded under the call's own gaps and lookahead, and recorded_series_value(key) reads it per chart bar, na where the tape has no row. Recording per chart bar, rather than re-deriving TradingView's report-time and fiscal-period rules from an event list, keeps the value exact by construction. pineforge-codegen 1.0.0 lowers the four calls onto the store by request key (lane XSYM-E); a run still needs the tapes, which scripts/run_strategy.py installs from PINEFORGE_REQUESTS_ROOT.
    • request.currency_rate / economic: Feasible — needs aux data. Would need the same kind of pinned series, which no lane records yet.
    • request.seed: Out of scope structurally. TradingView seeds are user-published time series hosted on TV's infrastructure; PineForge has no equivalent registry.
    • request.quandl: Out of scope by design. Deprecated upstream; not worth implementing.
  • Status: codegen lowers request.earnings / dividends / splits / financial onto the recorded series by their request key (<fn>|<symbol>|<field-or-id>|<period-or->|gaps_<on|off>|lookahead_<on|off>), and a request with nothing recorded stops the run where it is read. The engine store (lane XSYM-D) ships in engine v1.0.0 and the codegen lowering (lane XSYM-E) in pineforge-codegen 1.0.0.

barmerge.lookahead_on for lower-TF emulation

Currently ensure_supported_lower_tf_emulation_flags(...) throws when lookahead_on=true for a lower-TF request.

  • Feasibility: Out of scope by design. Mechanically possible — just remove the guard — but lookahead_on combined with synthesized intrabar bars exposes information from a not-yet-complete sub-bar. That is a backtest-validity footgun.
  • Future story: No plan to enable it. Higher-TF aggregation already honours lookahead_on for partial updates (which is the legitimate use case); the lower-TF synthesis path is fundamentally incompatible with backtest-honest lookahead.
  • Why not done yet: Intentional rejection, not an oversight.

TradingView-exact PRNG parity

math.random(...) byte-for-byte matching TradingView's stream.

  • Feasibility: Out of scope by design. PRNG is deterministic and reproducible across runs / platforms; that is the contract PineForge advertises for paid-parity. TV's PRNG is undocumented and would need black-box reverse engineering to match exactly.
  • Future story: None. Determinism is preferred over TV-byte parity. If TradingView publishes their generator we can revisit.
  • Why not done yet: Trade-off was made explicitly; see the pine_random SplitMix64 comment in math.hpp.

Verifying the surface yourself

The validation corpus under corpus/validation/ (the open corpus submodule; 312 probes at the pinned corpus commit) is the engine's own proof that this runtime delivers the surface listed above. Run bash scripts/run_corpus.sh to compile every generated.cpp against libpineforge.a and diff each probe's engine_trades.csv against the TradingView export shipped beside it; python3 scripts/verify_corpus.py --all grades the shipped trade lists by the canonical rubric. Its strict profile gates count + entry-price + exit-price + P&L within 1.0% / 0.01% / 0.01% / 1.0%; a strategy whose strategy.exit uses a trail_* parameter gets the production profile, which relaxes exit to 0.05% and P&L to 100%.

One probe, corpus/validation/anomaly-equity-mirror-strategy-equity-01, declares expected_tier: anomaly in its inputs.json: it pins the 1× equity margin boundary, where TradingView's own admissions are not deterministic, and grades anomaly instead of failing the sweep.

The benchmark under benchmarks/ grades PineForge, PyneCore and vectorbt with the same rubric; PineTS runs indicators only. Over its 201 slots (refreshed 2026-09-22) PineForge grades 200 excellent and 1 strong, PyneCore 133 excellent (benchmarks/results/summary.md).