What this page is. A complete map of the Pine v6 surface area that
libpineforge.aactually 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.hppprovides thePineMap<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.hppkeeps their geometry as data a strategy reads back. The runtime accepts continuous ordered-trade streams, but it does not emit alert events.
| 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. |
<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.
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). |
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).
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.
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++).
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.
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.
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.
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).
| 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).
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).
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.
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::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).
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>.
| 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 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.
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 ofrun(...); the subclass callsregister_security_eval(sec_id, requested_tf, input_tf, lookahead_on, gaps_on)for eachrequest.security()call site.evaluate_security(sec_id, bar, is_complete)— invoked by the runtime each time a security bar is ready (complete or partial underlookahead_on).clear_security(sec_id)— invoked whengaps_onproduces 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++, thenevaluate_security(...)withis_complete=true. - On a partial bar with
lookahead_on:eval_partial_count++, thenevaluate_security(...)withis_complete=false. - On a partial bar with
gaps_on:clear_security(sec_id). - Otherwise the partial bar is silently held until completion.
- On a complete aggregated bar:
- Lower-TF emulation (
lower_tf_emulation=true) synthesizes intrabar bars from the input bar viasynthesize_lower_tf_bars, which samples the OHLC path inratio + 1ENDPOINTS-distribution points, time-stamps each slice on a fixedrequested_secondsgrid, 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_tfis empty, or - a
request.security_lower_tfTF is finer than the input but does not satisfy the lower-TF emulation constraints above, or arequest.securityTF is finer than a chart fed only its own bars. A run given bars finer than the chart readsnaon every bar from arequest.securityfiner 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.
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<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)pine_color::* holds 17 named ARGB constants. Helpers:
new_color(c, transp)— set the alpha byte to the whole number nearest255 × (100 − transp) / 100, clamped to 0–255 (annatransparency is fully transparent).r(c),g(c),b(c)— channel bytes.t(c)— recovertransp(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.
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:
> 1for ratio aggregation,1for same TF,-1for calendar aggregation (month),-2when 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)— everyratioinput 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; }.
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).
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).
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.
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_errorThe 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.
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.
- Pine
map<K,V>has a dedicated header-onlyPineMap<K,V>runtime inmap.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 inferredna, 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++.
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 asstd::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 againststd::string.
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). |
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
ReportCextension or a side-channel CSV / JSON for an external renderer). Realtimealert(...)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 onReportC, 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.
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_fillsalready 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 acceptsvarip(0.10.4 rejects it): a historical bar executes once, so avaripkeeps its value likevar, and it is left out of thecalc_on_order_fillsrollback. - Why not done yet: the realtime distinction TV makes is meaningful only when the data feed is live.
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. Alibrary(...)script itself, andexportin a strategy, are still refused. The runtime needs no changes.
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 owngapsandlookahead, andrecorded_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, whichscripts/run_strategy.pyinstalls fromPINEFORGE_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/financialonto 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.
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_oncombined 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_onfor 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.
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_randomSplitMix64 comment inmath.hpp.
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).