The supported Python/JSON contract began in 1.0.0; the sections below identify later additions. This revision includes the 1.6.1 pair (2026-10-18).
Text marked since 1.5.0 describes diagnostic additions that 1.4.0 and
earlier releases do not have: the note severity, the user_message template
and the eleven codes that became notes.
Supported numeric conversions preserve missingness in the original type:
an integer na becomes floating na when a float destination consumes it.
Integer arithmetic propagates missing operands before a numeric conversion.
Consumed if/switch selections with both a value arm and a void arm report PF-E5088 at the selection; statement selections, implicit missing arms and uniformly value or void arms retain their policy, and requested void selections keep PF-E3102.
Legal Pine identifiers that collide with C++ keywords, engine names or emitter
temporaries are escaped in generated C++; they remain valid Pine names. The
existing pineforge_codegen.codegen.CPP_RESERVED export contains the explicit
reserved names. _safe_name also escapes emitter temporary patterns and
allocates a distinct name when the script already uses the preferred escape.
An identifier that does not collide keeps its spelling.
Generated state members, helper types and unallocated numbered member families
use the same escape rule, including locals of requested helpers.
bar and HUGE use the existing C++ escape; unique input titles keep their
Pine spellings. Repeated input titles use the ordinal override keys described
under "Input manifest and override keys".
The imported engine names report, report_sink, report_sequence, source,
native_calendar, pine_color, RunFailureCode, RunFailureInfo, QtyType,
CommissionType, XLoc, YLoc and normalize_timezone_for_posix are reserved
because using namespace pineforge; brings them into unqualified lookup. A Pine
type report, for example, becomes struct pf_safe_report and its references
use that name. These names live in codegen/helpers.py's
CPP_EMITTER_NAMES, which also reserves the generated C export
strategy_reads_trade_history. A Pine type or function of that name uses
pf_safe_strategy_reads_trade_history; the export keeps its public spelling.
The same set explicitly reserves the exports strategy_create,
strategy_create_checked, run_backtest, run_backtest_full, strategy_free,
report_free, strategy_set_input, strategy_set_override,
strategy_set_magnifier_volume_weighted, strategy_set_input_checked,
strategy_set_override_checked and run_backtest_full_checked. Pine identifiers
use pf_safe_<name>; these exports keep their public spellings.
CPP_STANDARD_MACROS reserves PINEFORGE_HAS_TRADE_HISTORY_POLICY_V1, so a Pine
variable of that name uses pf_safe_PINEFORGE_HAS_TRADE_HISTORY_POLICY_V1
instead of expanding the engine's feature macro in a declaration. It also
explicitly reserves PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1, using the same
pf_safe_ escape for a Pine identifier.
codegen/host_members.py's HOST_MEMBER_NAMES separately reserves the public or
protected host members the emitter spells, including
set_trade_history_streamable, inherited from BacktestEngine. All three sets
feed the existing CPP_RESERVED export; no new reserved-name API is introduced.
Since the 1.7.0 trade-history policy addition, generated libraries export
int strategy_reads_trade_history(void): 1 means the emitted code reads
closed-trade rows, and 0 means no such read was emitted. A closed-trade count
alone does not require those rows. For a non-reader, the constructor calls
set_trade_history_streamable(true) under
PINEFORGE_HAS_TRADE_HISTORY_POLICY_V1; a reader emits no policy call.
The order-shapes receipt's host_reads therefore includes the setter only for
non-readers. Older generated libraries can lack the export; its absence does
not establish that the script is a non-reader.
Translation also preserves these value contracts:
- A chart TA constructor argument that shadows a global constant or input follows its lexical binding; an unsized
ta.percentile_nearest_ranklength is lowered to the engine'sSeriesPercentileNearestRankclass, with one buffer per call site. A call inside anifappends only when it runs; no tape pins that conditional shape.PF-E5092remains the refusal for a series length ofta.sma, and its message names the function and says that TradingView accepts the call.PF-E5009keeps the plain wording, which makes no claim about TradingView, for every otherta.*function: the ones where TradingView refuses a series length too (ta.rma,ta.ema,ta.rsi,ta.atr,ta.dmi,ta.macd,ta.kc,ta.hma,ta.tsi,ta.rciand theatrPeriodofta.supertrend) and the ones with no recorded result. - A constant-expression input default can size a TA constructor; runtime resets still read the input getter.
line(na)andbox(na)produce the same missing handles as typed declarations initialized withna.- Literal
nain emitted strategy string slots uses the host's empty-string sentinel, includingfrom_entry,id,commentandoca_name. - Array bindings from helper tuple destructuring retain their element type for built-in method lowering.
strategy.default_entry_qty(fill_price)lowers to the engine strategy host's querydefault_entry_qty(price), the order size the default quantity settings give at that price; it needs a paired engine whose strategy host provides the query. The calls TradingView refuses are refused: no price, a second price, a price given both by position and by name, an unknown argument name (PF-E1088), and a string, bool, color or collection price (PF-E2083toPF-E2086, only for a type the syntax proves).PF-E5084, which refused every call until the host had the query, is retired: the code stays in the catalog and is no longer emitted. An entry with omittedqtykeeps engine default sizing.- An
iforswitchvalue reassigned to a Series updates the current slot in each selected branch. - Colliding direct nonpersistent scalar history declarations in ordinary functions have separate Series storage, including call-site clones, locals of different scalar families and a computed local initializer reading a same-named global with its own history. Embedded block, tuple and loop declaration collisions retain their existing storage gates.
- Direct
size,first,lastandgetreads ofline.all,box.all,label.allandlinefill.allborrow live handles in oldest-first order, with the normal array bounds checks. Non-var declaration aliases andfor h in …iteration take snapshots of those handles at the collection read; element setters and deletion still affect the live drawings. Whether a later creation in the same bar is visible through the alias is not yet measured against TradingView.var/varipaliases retainPF-E5086until a TradingView capture establishes their lifetime semantics. Declarations that yield a collection as a callable or selection-arm value, structural array mutations, collection reassignment, collection arguments/returns, history, pair iteration, user array-method overrides and other collection uses retainPF-E5086;polyline.allraisesPF-E5085pending an engine handle and arena. - Authored identifiers that collide with macros defined by the audited math, limits, C++ wrapper and paired engine headers use the existing C++ name allocator; authored input keys remain unchanged.
Availability: since 1.2.0, with engine v1.2.0's capability extension and
the receipt-based admission policy of its live runner.
The receipt proves declarations only, not general live-versus-batch equivalence.
Its strategy() positional arguments follow Pine signature order independently
of batch configuration extraction. Every unresolved argument is named in
unresolved. Runtime-lowered unpinned request sites retain their request kind,
symbol and timeframe in requests with feed: "unpinned", and in unresolved.
The original receipt alone retains conservative request/POOC/varip refusals.
From 1.3.0 on, confirmed-bar order metadata is an allowlist of modeled strategy calls and
arguments. Risk rules and unmodeled order arguments are named as unproven;
POOC account/sizing/slippage declarations outside the tested literal profiles also
produce an unproven marker. This changes receipts only, never trading code.
From 1.3.0 on, libraries also export an independently versioned confirmed-bar receipt;
engine v1.3.0's runner admits only its explicitly proven same-chart security shapes
and standalone close-only varip, not arbitrary requests or observed-tick calculation.
process_orders_on_close=true remains refused for every order shape and
request composition. Retained POOC order metadata and equivalence rows are
evidence for future admission, not admitted shapes; nullable price legs,
same-calculation brackets and empty entry IDs are not certified by the receipt.
Legacy libraries without a receipt warn and run, including their requests.
Realtime
builtins are conservatively named and refused: generated barstate.isrealtime
is false, and timenow reads the bar timestamp in both warmup and realtime.
barstate.islast, barstate.islastconfirmedhistory, last_bar_index and
last_bar_time are also named in unresolved: a stream's delivered endpoint
and last-bar flags differ from the completed batch's final bar.
All six builtin uses are refused including display-only use (plots, labels,
tables): the receipt records their use conservatively rather than certifying
that display-only code cannot influence strategy execution.
From 1.2.0 on, C++ compiled against headers that define both
PF_SETTINGS_API_VERSION and PF_CAPABILITIES_API_VERSION, as engine
v1.2.0's do, adds strategy_capabilities_api_version() (version 1) and
strategy_capabilities_receipt(handle, json, capacity, required, error, error_capacity).
The receipt uses the checked-settings status and buffer protocol: NULL/0 queries
return PF_SETTINGS_BUFFER_TOO_SMALL, required includes the NUL, and short
buffers return no partial JSON. Canonical JSON is emitted from strategy()
declarations and analyzed security/request sites, not guessed from a handle's
runtime behavior. It is immutable across settings changes and fresh/reused runs.
From 1.3.0 on, with engine v1.3.0, strategy_confirmed_bar_api_version() returns 1 and
strategy_confirmed_bar_receipt(handle, json, capacity, required, error, error_capacity)
uses the same immutable buffer protocol. Its JSON has version, requests
(including the expression), sorted orders classifications and
intrabar_persistence. Actual emitter foreign/heikinashi flags determine
feed classification and static registration metadata resolves constant and
single-call helper clocks. Symbol "" is a foreign-feed requirement, not chart
data. Runtime/input/mutable timeframes remain unresolved.
No new fields are added to the original receipt: old runners ignore the extra symbols and retain their original policy. A new runner paired with an old library without these new symbols also retains its original admission policy. These exports add metadata only; generated trading code is unchanged.
From 1.3.0 on, with engine v1.3.0, strategy_order_shapes_api_version() returns 1 and
strategy_order_shapes_receipt(handle, json, capacity, required, error, error_capacity)
uses the same checked-settings buffer protocol. Every generated strategy includes
this immutable receipt, whether closing-time order processing
(process_orders_on_close) is enabled or not. The declaration and constructor
settings echo describe the compiled defaults, not later input or setting overrides.
The canonical JSON contains version, process_orders_on_close, calls,
entry_ids, host_reads, settings and unmodeled. Each calls entry describes
one lowered Pine strategy.entry, strategy.order, strategy.exit,
strategy.close, strategy.close_all, strategy.cancel or strategy.cancel_all
site, in source order. It classifies each parameter that the call actually passes:
numeric values are default (absent), finite constants (literal), expressions
from a conservative non-missing-value allowlist (never_na), or potentially
missing (maybe_na). Text, enums, directions and IDs have separate classes.
IDs are classified without exposing their literal strings. Entry counts and
target relations include IDs from both strategy.entry and strategy.order;
they distinguish direction, shared IDs, repeated sites and exit ordering.
Loops and user functions or methods are marked repeatable.
Defaults are compared per lowered parameter, including the expanded optional
entry parameters. Dropped alert arguments do not affect the shape; an omitted
from_entry and from_entry="" both target entries globally. An exit lowered as
bracket cancellation records only its cancellation parameters; the receipt does
not relax existing Pine support checks. host_reads
lists generated execution-member references, excluding strings and comments;
unmodeled names other non-read-only strategy.* calls, including risk rules.
The receipt is a static description, not an admission decision or proof that an
arbitrary order shape is supported in a live run; engine v1.3.0 does not read it.
Existing capability and confirmed-bar receipts, including their order
classifications, remain unchanged.
Every generated strategy library also defines strategy_id_sites_api_version(),
which returns 1, and
strategy_id_sites_receipt(handle, json, capacity, required, error, error_capacity),
beside the order-shapes receipt and with the same checked-settings buffer
protocol, whether or not the script places orders. Its immutable canonical JSON
(sorted keys, no whitespace) is {"strategy":{"ids":{"freshPerBar":K}},"version":1},
where K is the number of lowered strategy.entry, strategy.order and
strategy.exit sites that can create an order id the script computes: sites whose
id is not a literal string. A literal or empty id adds nothing, and
strategy.close, strategy.close_all, strategy.cancel, strategy.cancel_all and
drawings are never counted. A site in a user function or method counts once per
static call of that function, transitively, so a function called twice from a
function called twice counts four times. A site in a for loop whose start, end
and step are known when the script is translated counts once per iteration. When a
while or for ... in loop, a for loop with a computed bound, or a recursive
call surrounds a computed id, the count has no static bound and the JSON is
{"strategy":{"ids":{"freshPerBarStatus":"unbounded"}},"version":1}, with no
freshPerBar key; a count above 4294967295 is reported the same way. A loop that
holds only literal ids does not make the count unbounded. K is an upper bound,
not an exact count: both arms of a conditional count, a method call counts by the
method's name, and a function parameter used as an id counts as computed. It
counts sites per execution of the script body, not per bar: the translator cannot bound how often
the body runs on one bar, because calc_on_order_fills (set by the script or by a
run-time override) runs it again after each fill. A host can size its order-id
bookkeeping from K times the number of executions it expects, and should treat
the demand as unbounded when calc_on_order_fills is on. The receipt describes the compiled script
and adds metadata only: generated trading code is unchanged, and a host that does
not look for the export is unaffected.
Version 1 has version, declarations, requests, requirements and unresolved
keys. Declarations include calculation cadence, close execution, magnifier,
standard-OHLC fill mode, limit verification, currency and clock settings. Requests
name their function, symbol, timeframe, gaps, lookahead, Heikin-Ashi transform and
feed source. Requirements name historical-only data and intrabar persistence;
nonliteral contexts remain explicit rather than silently assuming defaults.
See the paired engine's docs/strategy-capabilities.md for the exact schema and
live-runner refusal policy. No batch dispatch, strategy calculation, matching,
margin or numeric behavior changes. C++ compiled against headers without
PF_CAPABILITIES_API_VERSION, such as engine v1.1.0's, has no capability
extension.
Timeframe spellings differ by surface, and both are stable. A capability
receipt records a request's timeframe as the script wrote it: a literal
verbatim ("D" stays "D", "1D" stays "1D"), anything else as its Pine
expression (timeframe.period, a variable's name). Request
discovery and feed keys use the engine's spelling, in
which Pine's bare "D" / "W" / "M" / "S" are "1D" / "1W" / "1M" /
"1S" and any other text is kept. Discovery folds literal and computed
values (pineforge_codegen.request_discovery.canonical_timeframe); the engine
folds the requested timeframe when it looks a feed up, so a script's "D" and
"1D" read the same feed, keyed "1D". An input timeframe's default and
its override are raw: fold them the same way. To match a receipt's request to a
feed, fold the receipt's literal timeframe with that rule (folding twice
changes nothing); never map a feed key back to "D".
Availability: since 1.4.0, with engine v1.4.0's
PINEFORGE_HAS_RUN_FAILURE_CODES_V1 feature. The feature is optional in emitted
source; that fallback is not an exception to exact release pairing.
Generated no-data/other-symbol, collection, UDT, unsupported-request and
checked-string stops carry compile-time provenance through coded helpers.
A script's runtime.error always selects strategy_runtime_error; neither
its message nor an English message copied from another failure selects a
code. Named message arguments are passed, and an omitted message stays empty.
str.tonumber evaluates its argument outside the numeric parser's narrow
catches so a run stop propagates; only numeric parsing failures become na.
The generated run wrappers retain inner codes. A legacy setter's first failure
latches its English and setting_rejected identity for subsequent begins.
The paired engine provides strategy_get_last_error_code and
strategy_get_last_error_args beside strategy_get_last_error; its run harness
adds code and typed args beside failure error. An empty English message
does not make the run successful or authorize use of its partial results.
Without the feature, generated shims retain the legacy exceptions and English.
This is a runtime failure channel, distinct from the CompileError and
diagnostics contract of transpilation. Regenerate and relink for the new pair;
C ABI version 4 alone does not authorize reusing a 1.3.0 strategy library.
The paired 1.4.0 harness uses the checked settings API for generated libraries;
the 1.3.0 harness used the legacy setters. Some requests that previously
returned success now fail before execution: a shared Period key set to "7"
is setting_rejected with reason ambiguous_key, textual color "color.blue"
is expected_integer, and "15" outside choices [10, 20, 30] is
invalid_input_option. The 1.4.0 color manifest's string type and Pine-spelling
default did not describe the native setter's packed-integer encoding. Current
manifests use type: "color" with a packed integer default, as defined under
Input manifest and override keys. Validate
presets against the compiled settings, including types, bounds and options;
the checked path also refuses unknown keys and invalid numeric, boolean or
enum values. An input with supported: false rejects a supplied value with
setting_unsupported; leave it at its compiled default.
These examples do not imply that every invalid request used to succeed.
initial_capital="abc" and initial_capital="-1" already failed on 1.3.0;
the paired engine now reports setting_rejected with
expected_finite_decimal and value_below_minimum, respectively.
setting_rejected is catalog class input; setting_unsupported is class
unsupported. A deliberate runtime.error is strategy_runtime_error,
class strategy. The installed engine entrypoint returns exit 4 for all
three codes: exit 4 alone does not select a billing or refund decision.
Availability: since 1.5.0, with engine v1.5.0. This is the contract of a
report fix in the paired engine. The report fix
itself changes neither the generated C++ nor the transpiler's Python and JSON
contract: the wrappers still run the strategy and then ask the engine for its
report.
A generated run_backtest and run_backtest_full run the strategy and then
ask the paired engine for its report. The engine hands out the rows of the
attempt that call began. For a call that did not begin (a bar array or run
option it rejects, a handle that is not Ready, a calendar or timezone refusal,
a begin over a live stream) it hands out the empty report instead: every array
NULL, every count 0, overwritten and never freed. The report never answers a
refused call with the rows of an earlier run. A refused call on a fresh handle
already read empty; the rule extends that to a handle that has run before.
A refusal records its cause on the failure channel, strategy_get_last_error
(text) and strategy_get_last_error_code (code), and most refusals also set
strategy_last_run_status to 1. Not all do. A call the engine refuses before it
admits the begin writes the text and the code and leaves the run status as it
was: a bar array whose open, high, low or close is not finite (the source host
that a generated strategy derives from validates the array first) and a
repeated stream begin on a stream that is already realtime are two. A rerun on
a handle whose last run completed then reads status 0 together with a non-empty
error, the same status that the settings extension below calls "completed" for
an ordinary batch preparation failure. Read the error text, the error code
and the run status after every run. The run failed when the text is
non-empty, the code is non-empty, or the status is 1. The status alone misses
the refusals that leave it at 0, and the report alone does not tell a failed run
from a completed one. The paired engine's run harness applies this rule.
What a refusal does to the handle depends on why the call was refused, and the report fix changes none of it. A begin on a Completed, Failed or unconfigured handle, and a rejected bar array or run option, leave the lifecycle, the run identity and the retained rows as they were. A calendar or timezone failure inside the begin itself comes after the consumed run-number high-water moved and fails the lifecycle; a calendar or timezone parse failure at configure time comes earlier, fails the lifecycle there, and leaves the high-water where it was. A batch begin over a live stream, or a begin made from a callback, is a contract failure that latches Failed, so the stream cannot be driven on afterwards, although its snapshot stays readable. A repeated stream begin on a stream that is already realtime is the exception: the engine refuses it before it admits the begin, latches nothing, and the stream stays running.
A run that began owns its rows whatever became of it: a completed report and the
partial rows of a run that began and then failed are handed out as they are, and
the failure is on the same channel. The engine's explicit stream snapshot,
strategy_stream_fill_report, asks for the rows' own owner. A live stream's
rows survive a batch call refused over it, which itself reads empty, and the
batch reader stays empty after the snapshot until the next run call. When no
live attempt owns rows (a refusal with nothing running) the snapshot is the
empty report. A run call made from inside a running run's callback reads empty
in the same way, while the outer run, which began, hands out its own rows when
it ends. The snapshot returns 0 when it wrote its output and -1 for a NULL
handle or output and for an exception contained at the C boundary. In that
case the output may hold the arrays built before the exception, so pass a
zero-initialized pf_report_t and release it with the library's report_free
(the engine's strategy_native_report_free_v1 for a host that links the kernel
archive on its own) whatever the result.
Recorded outputs (strategy_outputs_*), on an engine that provides them, keep
their own record, the last run that began, and their own documentation. The
rule above does not cover them and this section does not change them. Engine
v1.4.0 does not provide them.
This needs an engine release that carries the report gate: the first release that contains this change. Against an older engine, a rerun on a reused handle can return the previous run's rows together with the refusal, so read the failure channel and the status after every run. Regenerate the strategy C++ and relink against the new engine as for any pair change.
From 1.1.0 on, C++ compiled against an engine providing
pineforge/checked_settings.hpp, as engine v1.1.0 does, adds
strategy_settings_api_version() (version 1), strategy_create_checked,
strategy_set_input_checked, strategy_set_override_checked,
strategy_get_effective_settings and run_backtest_full_checked to generated
libraries. The existing symbols and their successful default computations are
unchanged. All generated factory, setter and batch entry points contain both
standard and non-standard exceptions; checked variants return a status and an
optional caller-buffer error instead of silently substituting defaults.
Checked inputs are keyed exactly as the generated getters. The first explicit
title keeps that title as its key; later inputs of the exact same title use
<title>#2, <title>#3, etc., in declaration order, across groups and input
types. Untitled inputs keep their binding-name fallback (or the empty key).
The manifest's title and receipt's name publish these opaque override keys;
consumers must use them verbatim. Metadata lists every declared input (including
inline inputs), its storage type, default, options, declared numeric limits and
UI step. Unknown/ambiguous keys, invalid booleans/enums/options, numeric suffixes,
integer overflow, non-finite numeric strings and limit violations are refused
before mutation. Legacy setters stay permissive. Receipts read the actual getters
and effective overrides, in source order, with canonical string values. Query
the required JSON buffer size with NULL/0 before reading it. Configure checked
settings before execution starts. The exact C signatures, statuses and buffer
contract are documented in the paired engine's pineforge/pineforge.h and
docs/checked-settings.md. Without that optional header, codegen still emits
the legacy ABI and exception containment, preserving old-engine builds.
A legacy setter that throws permanently invalidates that generated strategy
handle. Its first failure message is retained independently of the engine's
last-error buffer: auxiliary-feed configuration and other diagnostic-clearing
calls cannot clear the failure. Both batch and stream begins refuse execution,
and strategy_get_last_error reports the original setter failure. Legacy batch
reports remain empty; checked batch returns PF_SETTINGS_RUN_FAILED. Free and
recreate the handle to recover. Legacy setters that do not throw keep their
existing permissive behaviour.
Engines v1.1.0, v1.2.0 and v1.3.0, the pairs of codegen 1.1.0, 1.2.0 and
1.3.0, provide that header. Settings
helper references are root-qualified and guarded by PF_SETTINGS_API_VERSION;
old headers retain the standard-exception fallback and legacy batch precheck.
Paired batch and stream refusals report NOT_COMPLETED through the shared native
begin. Checked setters and receipt queries on a latched handle also return
PF_SETTINGS_RUN_FAILED. An active stream keeps its begin-time settings after
a mid-stream legacy setter throws; the sticky failure applies to subsequent
begins.
Immutable identifier defaults for input.enum now resolve to their declared
member instead of reading an uninitialized script member (the old default was
incorrectly member zero). Both the getter and deterministic receipt use that
literal. Unfoldable defaults remain unsupported by checked setters and their
receipt uses na without reading script state; the legacy getter is unchanged.
Enum options use the same immutable initializer resolution as defaults:
an identifier must have one non-reassigned declaration before the input call,
and every declaration in its chain must be neither var nor varip.
Every resolved option and the default must be members of the same enum for
checked support. Choices retain their written order and member-index encoding.
Stream begin fails on any script-preparation exception with its message and
NOT_COMPLETED; ordinary batch preparation failures keep their historical empty
report, diagnostic and completed status. Legacy exception text is standard-
library-dependent, and legacy run catches zero their output report.
From 1.1.0 on, transpile_full()'s result and gate/glue.py's
transpile_json success envelope carry a requests key, and each
input.symbol entry of the input manifest has "kind": "symbol" (its other
fields are unchanged). Request discovery does not change the emitted C++.
requests lists, in source order, every request site that reads another
symbol's feed: a request.security of another symbol whose value can reach a
trade. A run supplies such bars as one feed per (symbol string, timeframe) and
the engine matches both byte for byte, so each entry states them as the
generated code computes them before the first bar:
{"line": 7, "fn": "request.security",
"symbol": {"kind": "input", "title": "Other symbol", "default": "BINANCE:ETHUSDT"},
"timeframe": {"kind": "chart"},
"lookahead": false, "gaps": false, "ignore_invalid_symbol": false}| Field | Value |
|---|---|
line |
The 1-based line of the request.security call. A helper's request is listed once per symbol and timeframe its call paths pass, each at the request's line. |
fn |
The Pine function, "request.security". |
symbol |
{"kind": "literal", "value"}: the exact key, exchange prefix and suffix kept ("BINANCE:ETHUSDT.P"). {"kind": "input", "title", "default"}: an input.symbol, input.string or string input() (ticker.standard / ticker.inherit of one too); the key is the run's value of the input, overridden under title (the manifest's override key), else default. {"kind": "computed", "expr"[, "value"][, "inputs"]}: expr spells the expression as written, value is its key at the inputs' defaults when literals and inputs compute it (string +, ==, !=, and, or, not, ?:; never the chart's own syminfo.* strings), and inputs names the override keys of the inputs it reads. {"kind": "unresolvable", "expr"}: a request whose symbol, timeframe or expression PineForge cannot key before the first bar; no feed serves it, and the run stops where its value is read. |
timeframe |
{"kind": "literal", "value"} in the engine's spelling: whole minutes ("240"), <n>D, <n>W, <n>M or <n>S ("1D"), Pine's bare "D" / "W" / "M" / "S" as "1D" / "1W" / "1M" / "1S"; other text is kept as written and no feed matches it. {"kind": "chart"}: timeframe.period, timeframe.main_period or "", the chart's timeframe (a run that reads another symbol's feed takes the chart's bars unaggregated). {"kind": "input", "title", "default"}: an input's raw value, spelled by the same rule, "" the chart's. {"kind": "computed", "expr"[, "value"][, "inputs"]} as for the symbol, a switch of one-expression arms included (no matching arm and no default is the chart's); an empty value is the chart's timeframe. |
lookahead, gaps |
Booleans: barmerge.lookahead_on, barmerge.gaps_on. |
ignore_invalid_symbol |
false when omitted, else its value (at the inputs' defaults), null when not computable. |
column |
Present for a request.footprint payload: the feed column it reads ("fp_delta_100_70"). |
A request whose value reaches only plots, alerts, tables or logs is lowered to
na and reads no feed; a request in a helper nothing calls never runs. Neither
is listed. Nor are requests that read no other symbol's bars: the chart's own
symbol (syminfo.tickerid, syminfo.ticker and ticker.* of them), a
request.security_lower_tf of another symbol (the run stops where it is
evaluated), and request.earnings / dividends / splits / financial
(recorded series, not bars). A symbol string equal to the chart's at run time
reads the chart.
This is the contract that 1.0.0, released 2026-09-30, implements, and 1.0.1,
released 2026-10-02, implements unchanged. 1.1.0, released 2026-10-04,
implements it with the generated settings extension and request discovery
above added: transpile_full()'s result and the JSON success envelope gain
requests, and input.symbol manifest entries gain kind. No argument,
result key or envelope is removed or renamed. 1.2.0, released 2026-10-05,
implements it with the compiled execution capabilities and the diagnostic
codes described here added: each Diagnostic and each JSON diagnostic gain
code and args, and pineforge_codegen exports diagnostics_catalog()
and render_diagnostic(). No argument, result key or envelope is removed or
renamed. 1.3.0, released 2026-10-06, implements it unchanged, with the
confirmed-bar and order-shapes receipts described here added to the generated
C++: no argument, result key or envelope is added, removed or renamed.
The last 0.x release, 0.10.4, has
neither the libraries argument nor the diagnostics key described below.
The supported programmatic entry points are the Python functions
pineforge_codegen.transpile and pineforge_codegen.transpile_full, plus the
gate/glue.py transpile_json protocol shipped in the Pyodide package. The
pipeline classes shown in the README
are for advanced inspection and are not part of this 1.0 stability promise.
transpile(pine_source: str, *, check_support: bool = True,
filename: str = "<input>",
libraries: Mapping[str, str] | None = None,
history_capacity: int | None = None,
drawing_capacity: int | None = None) -> str
transpile_full(pine_source: str, *, check_support: bool = True,
filename: str = "<input>",
libraries: Mapping[str, str] | None = None,
history_capacity: int | None = None,
drawing_capacity: int | None = None) -> dictThe same keyword-only capacity options are accepted by
transpile_with_request_inventory() and root gate/glue.py.
The JavaScript worker template in scripts/worker-template.mjs passes only
the source and does not forward either option.
history_capacity is an exact integer of at least 2 or None;
drawing_capacity is an exact integer of at least 1 or None.
Booleans, floating values, strings and smaller integers raise PF-E5094
with option and minimum arguments.
History retention depends on the source and these parameters, never the
number of bars or trades. With history_capacity=None, existing Pine
max_bars_back directives and literal-sized stores keep their capacities.
Without a directive, a bare history with a statically proven offset exceeding
its former 500-slot capacity retains offset plus one slots, within the existing
evaluated-offset range. A nonconstant offset without a directive selects
DEFAULT_DYNAMIC_HISTORY_CAPACITY, defined once in codegen/capacities.py,
whose value is 5000, this package's default retention for a script that
reads history at an offset it does not state as a constant.
This is a retention default, not a new refusal threshold: at default settings,
reads beyond the retained capacity preserve their existing missing-value
behavior, including scalar na. The inferred sizes apply to bare series and to
requested-helper map entries. Collection snapshots and lazy held-source
histories keep the size they had before: a collection snapshot its largest
literal offset plus one (at least 2) or, for a nonconstant offset, the depth of
a max_bars_back directive or the engine's 500-slot default; a lazy held-source
history its literal length plus one or, for a runtime length, the 500-slot
default. A lazily evaluated call with a literal length, such as the
ta.roc(close, 200) of close > open and ta.roc(close, 200) > 0, keeps the
201-slot store its own length requires: history_capacity neither shrinks nor
refuses it.
A collection-snapshot read at a nonconstant offset also counts as a nonconstant offset for this inference, although snapshots have their own rings: without a Pine directive, bare series then retain 5000 slots instead of 500, increasing their retained sample storage tenfold. A constant collection-snapshot read does not raise bare-series retention.
An explicit history_capacity=N replaces fallback retention in bare series,
collection snapshots and numeric/string requested-helper histories. Existing
Pine directives and literal-sized stores are never reduced by a smaller
fallback. With no larger Pine directive, every collection-snapshot ring has
N slots under the option even when its reads use small literal offsets.
A statically proven offset at or above N raises PF-E5095 with
offset and capacity arguments. A nonconstant offset is evaluated once,
checked by the existing 0..10000 guard before narrowing, then compared with N
before the history read. An offset at or above N stops the run with the
existing engine code pine_runtime_limit and typed arguments
{"limit":"max_bars_back","max":N}. Its English text reports the evaluated
offset and selected capacity; the code and arguments identify the failure.
The stop's max is the operator's slot count N, so a read at offset k fits
when k < N, and its text has no Error on bar prefix.
The same rule covers collection snapshots, requested histories, numeric/string
helper-map reads and the length-1 address of runtime held-source histories.
An in-capacity read during warmup retains its existing missing-value behavior.
Generated code uses the existing _PF_LIMIT_STOP compatibility shim; older
engines without coded failures retain its std::length_error fallback.
An integer outside the native Series constructor's signed-int domain raises
PF-E5097 with a capacity argument instead of emitting a narrowing conversion.
There is no additional upper bound on history_capacity within that native
integer range; memory grows with N times the number of retained series.
When a Pine max_bars_back=D directive exceeds the operator's N, stores keep
D slots, but both offset checks still use N alone: a constant offset at or
above N raises PF-E5095, and a nonconstant offset at or above N stops at
runtime with max=N, even if it is smaller than D.
Capacity inference scans the whole script, including functions that are never called, so a subscript in an uncalled function can raise inferred retention or, with an option, cause the constant-offset refusal.
Pine's max_lines_count, max_boxes_count and max_labels_count remain
collection targets, defaulting to 50. Their existing collection timing and
held-handle rules are unchanged. drawing_capacity denotes an operator bound
on live drawings per collected type, after that collection, including the new
drawing. When a line, box or label creation or copy leaves its kind's live count
greater than N, the run stops with pine_runtime_limit and typed arguments
{"limit":"live_drawings","max":N}. Each kind has its own count; linefills
are outside the bound. Reaching a Pine declaration's count remains ordinary
TradingView collection, not an error. Pinned drawings retain their existing
protection and still contribute to the live count.
Typed drawing overflow requires the paired engine release whose runtime-limit
catalog includes live_drawings; on an older engine the drawing overflow is
not typed as this limit. None preserves existing arenas and collection.
An integer outside the helper's signed int64 maximum argument domain raises
PF-E5098 with a capacity argument before C++ is emitted. Those arenas
currently use the signed 32-bit ID-domain maximum as their legacy storage
argument; this is not a true unlimited arena policy. Replacing it requires
an explicit engine policy, including physical reclamation of deleted records.
transpile() returns one complete C++ source string. It raises
pineforge_codegen.errors.CompileError for a located parse, support, analysis,
or generation error; its diagnostics attribute carries Diagnostic objects.
filename appears in their source locations and in str(error).
transpile() returns no nonfatal diagnostics, neither warnings nor (since
1.5.0) notes; use transpile_full() to read them. The diagnostics of a
CompileError can carry them beside its errors.
A counted-loop counter is visible in its loop's end and step expressions
and its body, but not after the loop. A for ... in binder is local to the
loop body. Reading either outside its scope raises analyzer PF-E2054 at
the identifier, unless an enclosing declaration or builtin supplies that
name. Reassigning a name with no visible declaration retains PF-E2020.
transpile(), transpile_full() and transpile_with_request_inventory() check
the generated C++ allowlist even with check_support=False and raise
CompileError with at most 20 fixed reasons at filename:1:1 when it reports
findings.
libraries maps a Pine library's import path to its source
({"user/name/version": source_text}); each import the script uses is inlined
from it before the support check. With None, the default, the sources are
read through the script's own requests manifest when $PINEFORGE_PINE_LIBRARIES
and $PINEFORGE_REQUESTS_ROOT name one. An import with no source is refused by
name, except one whose alias is ta, math or str and that names only that
namespace's built-ins, which is a no-op.
Library imports are supported only when the library source is supplied;
otherwise compilation fails with the diagnostic above.
Unresolved imports carry a hint about supplying or inlining the library source:
PF-E2040 without a resolver reason, PF-E2041 with one. The message text and
named arguments are unchanged. PF-E2018 and PF-E2019 remain catalog entries
for the forms without a hint.
transpile_full() runs the same translation once and returns these keys on
success:
| Key | Python value | Meaning |
|---|---|---|
cpp |
str |
The generated source, identical to transpile() for the same inputs. |
inputs |
list[dict] |
Input manifest in global source order, one entry per global-scope input call, including inline calls. |
strategyParams |
dict |
Values extracted from the strategy(...) declaration; a nonliteral value can be None, and an omitted argument is absent even where the C++ applies a default. |
diagnostics |
list[Diagnostic] |
Nonfatal diagnostics with source locations: warnings (Level.WARNING) and, since 1.5.0, notes (Level.NOTE). |
requests |
list[dict] |
Since 1.1.0: the other symbols' feeds the script reads, one entry per request site; see Request discovery. |
An error still raises CompileError; there is no partial success dict. The
diagnostic severity enum values are "warning", "error" and, since 1.5.0,
"note" (Diagnostic codes). A successful
transpile_full() returns warnings and notes; a CompileError can carry
errors together with either nonfatal level.
Each manifest entry has title (string), type (one of int, float,
bool, string, source, enum, color), default (a literal scalar or
None) and supported (boolean; the manifests of releases up to 1.3.0 lack it).
It may also have min, max, step numeric values and an options list. Since 1.1.0,
an input.symbol entry also has kind equal to "symbol". An optional field
is omitted when its argument is absent or cannot be reduced to the supported
literal form.
From 1.4.0, supported and the values below come from the checked-settings
descriptor, subject to the explicit retained limits below (what
the compiled strategy's strategy_get_effective_settings reports): the manifest
is read from the descriptor that generates the receipt, and PyPI's
transpile_full() and the npm package's Pyodide/glue success envelope carry
them identically.
supportedisfalsefor an input the compiled strategy cannot honour: a string input whose default or a choice is neither a string literal nor a known built-in constant (size.small,position.top_right,na, a reassigned name), or aninput.enumwhose default or choices do not resolve to members of one enum through direct members or immutable declarations before the call. Hide such inputs, exclude them from sweeps and optimization, and do not send a value: the strategy uses its compiled default, and the paired engine rejects a supplied value withsetting_unsupported. Usesupportedto determine eligibility rather thanoptionsemptiness: every unsupported string input hasoptions: [], with or withoutoptions=, and so does a supported input writtenoptions=[]. A true flag does not validate a value's type, bounds or choices, or detect duplicate override keys.- A string input (
input.string,.timeframe,.session,.symbol, ...) publishes the lowered strings: a built-in constant its runtime value (alert.freq_allis"all",currency.USD"USD",format.price"price",order.ascending"ascending",session.regular"regular") and a named string constant its value. An unrepresentable default is""; a representable default is kept when only a choice is unsupported. The descriptor decodes only the emitter's C++ string-literal escape grammar, preserves literal Unicode and escaped backslashes, and stops at the first embedded NUL, matching nativestd::string(const char*)construction. The emitter spells NUL as fixed three-digit octal\000(one backslash), including before digits. This is current manifest/receipt consistency, not lossless storage of a string containing NUL. - A source input (
input.source(hl2), a plaininput(close)) publishes its series asdefaultand the nine native sources (close,high,hl2,hlc3,hlcc4,low,ohlc4,open,volume) asoptions; a plaininput(close)keeps itsstringtype. - An enum input publishes its
Enum.memberchoices asoptionsin written option order, or enum declaration order when options are omitted. It keeps itsEnum.memberdefault, which the receipt encodes as the member's index (map it through the receipt'soption_values). - A color input (
input.color) hastype: "color". Its literal-foldabledefaultis an unsigned packed0xAARRGGBBinteger (0 through 4294967295), whereAAis opacity: 255 is opaque and 0 is transparent. Named Pine v6 constants, hex color literals, andcolor.new/color.rgbwith literal arguments publish the value the generatedget_input_int64default uses, including transparency. For example,input.color(color.red, "Line color")publishes{"title":"Line color","type":"color","default":4294063685,"supported":true}(0xFFF23645);color.new(color.red, 50)publishes0x80F23645as an integer. Preserve the unsigned value when reading the JSON. Pass its decimal representation to the native input setter; a Pine expression such as"color.red"is not a native input value. This corrects thestringtype and Pine-spelling default published through 1.7.0.supportedand other input fields, the emitted C++ and the native receipt's storage type (int, 64-bit) and historicalkind: "string"are unchanged. An unfoldable color default retains its previous unpublished value (None/JSONnull); accepted default expressions are unchanged. - A typed numeric input (
input.int,.float,.price,.time) publishes numericdefault,min,max,stepand the numeric choices of anoptions=[...]dropdown where the receipt's value is a literal: a signed number (-2.5), a named constant (LEN = 14,minval=-RATIO) ortimestamp("2024-01-02T00:00:00")of a string literal. A plaininput(-5)orinput(-2.5)is typedintorfloatby its literal; these signed forms retain a double getter and afloatreceipt type. A plaininput(5)uses an int getter and receipt. Aninput.booldefault is a boolean when the descriptor lowers it to literaltrueorfalse, including an inlined constant;not trueremains a run-time expression. - Not published, because the receipt computes it at run time: arithmetic over
constants (
LEN * 2), a call (math.pow(2, 3)),timestamp(year, month, ...)(it reads the symbol's time zone) andnot true. Such adefaultisNoneand such aminormaxis omitted. The whole numericoptionsfield is omitted when any choice is unfoldable. These gaps are pinned intests/test_input_metadata_receipt.py'sKNOWN_LIMITS; its test view maps an absent options field to[], which is not the public manifest shape. Apart from the source-input case above, a plaininput(...)whose default is not a literal is typedstringwith aNonedefault.
For a script whose explicit input titles are all different, no key is renamed
or removed; a later input of a repeated title gets <title>#<n> as described
in rule 1 below.
The title is the actual override key read by the emitted C++:
- The value of an explicit compile-time constant
title, if supplied. The first occurrence keeps that value. Later occurrences of that exact title append#<n>in declaration order, starting at 2 and taking the next ordinal whose key is neither an authored explicit title anywhere in the script nor a key already assigned. Groups and input types do not restart the count. - Otherwise, the name of the declaration containing the input call.
- Otherwise, the empty string
"".
For example, length = input.int(14) has key "length", and
ta.ema(close, input.int(9, "Fast")) has key "Fast". The manifest includes
both declared and inline global-scope calls, one row per call. Two inputs
titled "Length" publish "Length" and "Length#2" when no input authors
"Length#2"; a "Length" override sets only the first, and "Length#2" sets
only the second. If another input authors "Length#2", it keeps that key and
the repeated "Length" gets "Length#3", whether the authored "Length#2"
comes before or after it.
If several calls still share a key because of unchanged untitled fallbacks
(including a fallback matching an explicit or assigned key), those rows retain
the same title and their per-input supported flags do not reflect the
collision. The translator warns about the colliding inputs. The legacy setters
apply a shared override to all matching inputs; the checked setters refuse the
key as ambiguous.
On the paired 1.4.0 engine's normal checked path, a supplied shared key fails
with setting_rejected, reason ambiguous_key; a completed run with no such
override still reports the whole fingerprint as null. This differs from
legacy-path unresolved provenance rows, which may remain inside a fingerprint.
Give those remaining colliding inputs different explicit titles.
An explicit title that is not a compile-time string constant raises
CompileError.
gate/glue.py defines
transpile_json(source: str, *, history_capacity: int | None = None, drawing_capacity: int | None = None) -> str.
It returns a
serialized JSON string. The shipped Pyodide worker runs that glue. Its
success and compile-error envelopes are:
{"ok":true,"cpp":"...","inputs":[],"strategyParams":{},"diagnostics":[],"requests":[]}{"ok":false,"error":"<input>:2:1: ...","diagnostics":[{"line":2,"col":1,"message":"...","severity":"error","endCol":10}]}ok is a boolean. On success, cpp, inputs, strategyParams and (since
1.1.0) requests have the same meanings as in transpile_full(), and
diagnostics contains warnings and, since 1.5.0, notes. On a CompileError,
error is str(error) and diagnostics contains the error's diagnostics,
nonfatal ones collected with the errors included; cpp, inputs,
strategyParams and requests are absent. Every JSON diagnostic has 1-based
integer line and col, a message string, and severity equal to
"warning", "error" or, since 1.5.0, "note". endCol is included when the
source location provides it. A diagnostic hint, when present, is appended to
message after —. Since 1.2.0 every JSON diagnostic also has code and
args; since 1.5.0 it also has user_message, the short ICU template
(Diagnostic codes). The glue catches
CompileError; an unexpected Python exception may propagate instead of
producing an envelope. The JSON entry point does not accept a filename
argument.
Methods whose first parameter is never referenced in their body report the fatal analyzer diagnostic PF-E5087 at that parameter, with a hint to use it or declare a function instead; later unused parameters and ordinary functions remain accepted.
Pine v6 call arguments must put all positional arguments before named
arguments. PF-E2074 is a fatal analyzer diagnostic at the first positional
expression that follows a named argument in the same call. It covers calls
and strategy declarations before argument rewriting or unused-code removal,
including v6 library bodies, and remains enforced with check_support=False.
Nested calls have independent argument lists; all-named calls may name
parameters in any order. Imported v5 libraries retain their existing policy.
Callable value reads follow declaration order. A Pine v6 function or method
reading a later local without an earlier visible binding reports PF-E2075
at that identifier; a later direct global retains PF-E2053. Earlier outer
bindings remain visible before a local shadow is declared.
A Pine v6 for loop whose counter start has positive int evidence reports
PF-E2079 for a float-typed step. This fatal analyzer diagnostic points at the
step expression, with the hint "Use an int step or a float counter."
Unknown starts are not refused by this diagnostic.
Translator changes must follow the
emission invariant. Missing lowerings require
a PF-E refusal at the construct's source location where that rule permits
refusal. TradingView-valid scripts that already transpile and compile keep
compiling while their semantics are implemented. The existing diagnostic
catalog and Python/JSON error envelopes carry located refusals.
Every Diagnostic (in transpile_full(...)["diagnostics"], in a
CompileError's diagnostics, and in the glue envelopes) carries the stable
code and args introduced in 1.2.0. Since 1.5.0 it also carries
user_message, and its severity can be note:
code: a stable stringPF-<S><NNNN>.EandWare historical identity prefixes, not the severity: the eleven codes that became notes in 1.5.0 keep theirPF-Wcodes. Its first digit is the area that spells the text:0source (lexer, parser, limits),1support checker and requests,2analysis,3request.security,4libraries and imports,5code generation,6array and matrix history,7ta.*. The support checker reports what it refuses inside aswitcharm as a warning:PF-W1nnnwithnnnbelow 500 isPF-E1nnnthere.- Severity (
Diagnostic.level, theseverityof a glue diagnostic and of a catalog entry):"error","warning"or, since 1.5.0,"note". An error is fatal and raisesCompileError. A warning and a note are nonfatal: a script that transpiles with either returns its C++ (ok: truein the glue envelope), and aCompileErrorcan carry them beside its errors. The catalog declares each code's severity and the diagnostic carries it; read it, never infer it from theE/Wprefix or from the words of a message ("visual only", "display"). A note is a classification, not a statement that results are unaffected: PF-W1505, a note, describes EMA seeding that can differ from TradingView at the start of a run. args: an object of named values, raw: identifiers, types, keywords and Pine spellings as strings, counts as JSON numbers; never quoting, backticks or a formatted number.user_message(since 1.5.0): one short English ICU template, identical on the catalog entry,Diagnostic.user_messageand the JSON diagnostic. It uses only the diagnostic's existingargsnames and may be constant. It is the raw template overargs, not pre-rendered English; it does not replacemessageandhint, and it never substitutes the unbounded raw diagnostic as its whole sentence. For example,"{name}() does nothing in a backtest."travels withargs.name = "plot"; a receiver renders or translates it.
diagnostics_catalog() returns the catalog, which ships as
pineforge_codegen/diagnostics_catalog.json (schema
pineforge-diagnostics-catalog/v1, whose id the 1.5.0 additions do not
change) and is attached to each GitHub release
(diagnostics_catalog-v1.3.0.json for 1.3.0).
Per code it gives severity (error, warning or, since 1.5.0, note),
area, the English ICU MessageFormat message template, the hint template
or null, a one-line explanation, args and, since 1.5.0, the short
user_message template. args gives each argument's
kind — identifier, type, keyword, number, vocab
(an English word or phrase the transpiler picks from the closed set listed in
values, which an application may translate) or text (open English text the
transpiler builds, such as a nested reason; shown as is). The templates use
simple {name} arguments and ICU apostrophe quoting ('' is one apostrophe,
'{' a literal brace); a string argument renders as is and a number in plain
decimal digits. render_diagnostic(code, args) returns the English
(message, hint), which equal the diagnostic's message and hint byte for
byte; in the glue envelope message is the message, plus — and the hint
when there is one.
PF-E2081 identifies the removed when argument on strategy.entry,
strategy.order, strategy.exit, strategy.close, strategy.close_all,
strategy.cancel and strategy.cancel_all in a Pine v6 source. Its
args.key names the function and args.name is "when"; the location
covers the keyword name. The message explains that Pine v6 removed the
argument and recommends placing the call inside an if statement.
Other unknown names keep PF-E2027 and its generic wording. A v5 source
keeps its existing refusal: generic PF-E2027 with support checking,
or the unsupported-version diagnostic PF-E2002 when support checking is
disabled.
From 1.5.0 on, receivers translate by stable code using their own locale
catalogs; the English user_message template is the translation source and
fallback for an unknown code. An unknown code is shown at its declared
recognized severity. An unknown severity is read as warning, including when
the code is known; code prefixes do not override that rule. Render the
template with the separate argument values as text, not as new template syntax
or markup.
The receiver examples in tests/fixtures/diagnostic_notes/receiver.json
specify this compatibility rule; they are not evidence of an app deployment.
Since 1.5.0 the reviewed warning-to-note migration is exactly PF-W1505,
PF-W1508, PF-W1509, PF-W1525, PF-W1526, PF-W1527, PF-W1528, PF-W1529,
PF-W1530, PF-W1531 and PF-W1532, each a warning in 1.4.0. All other
severities and all stable codes are unchanged. At that migration, every existing
message, hint, explanation and argument definition was unchanged, and no
existing key of a Diagnostic, a JSON diagnostic, the catalog or the envelopes
was renamed or removed: severity gained the value note and user_message
was added, while code, args,
message, hint, line, col and endCol kept their values. PF-W1505's
message, hint and explanation describe both EMA seeding modes and the
series_origin_ms run option (a text change only).
The prior catalog and pin remain explicit legacy fixtures beside the exact
delta in tests/fixtures/diagnostic_notes/. PF-W1509 remains a legacy
template alias: the existing text classifier selects PF-W1508 when those
two templates tie. The migration does not invent a new emitted identity.
Those eleven are the only codes the catalog declares as notes; a message that
says "visual only" or "display" does not make its code one. PF-W1508 and
PF-W1509 describe a call PineForge skips as visual only (plot(...),
table.new(...)). PF-W1525 to PF-W1532 describe an earnings, dividends,
splits, financial, footprint or other-symbol request whose value reaches only
display and alert sinks and is lowered to na. PF-W1505 describes the
ta.ema seeding caveat, a note by the explicit acceptance of the
initialization-note foundation. Five diagnostics that also speak of
visual-only or display-only handling remain warnings: PF-W1502 (a color(...)
cast), PF-W1506 (color.from_gradient), PF-W1507 (a drawing setter that drops
color, style or size), PF-W1078 (a visual or style constant) and PF-W1552 (a
request.security_lower_tf whose symbol can be another symbol's and whose
value reaches only display and alert sinks). Every other diagnostic keeps its
declared severity as well: executable missing feeds, partial risk support,
ignored trading values, repainting, numerical approximations, lossy values and
invalid arguments stay warnings or errors.
PF-W1505 is a static note: the support checker emits it at each ta.ema call
it visits, with no condition on the run, an input or the timeframe, and nothing
in this revision suppresses or qualifies it per call. Its text describes
both run modes; changing this text does not change generated C++ or seeding.
By default, ta.ema seeds from the first value of the data PineForge runs and
is finite from bar 0. The same holds for ta.macd and ta.tsi, which embed
EMA. This seed matches TradingView when its chart had history before
your range, as a normal chart does: the seed difference has decayed by then.
When the TradingView reference run starts where your range starts, TradingView
instead returns na for the first length-1 bars, seeds from the SMA of the
first length bars at bar length-1, and then applies the EMA recursion.
ta.kc and ta.kcw already start that way in every run: the generated code
applies the warm-up to their EMA basis, so their middle band is na until the
Pine length is warm, including bar 0 (tests/test_kc_middle_band.py pins this
against a standalone ta.ema).
An engine that has the run option series_origin_ms takes it through the run's
syminfo: epoch milliseconds of the open time of the first bar of the series the
TradingView reference ran on; an engine without it ignores the key. When that
origin is your range start, declaring it gives ta.ema, ta.macd and ta.tsi
the same na interval, SMA seed and subsequent recursion. It also starts
request.security series at the first whole
higher-timeframe bucket at or after the origin; plain security expressions
stay na until the first completed higher-timeframe bar. A wrong declared
origin gives wrong values. The option has no defined meaning in a live run
and must not be set there. ta.rsi, ta.rma and ta.atr already seed from
the SMA; this note does not apply to them. No migration step is required;
undeclared runs are unchanged.
For a measured TradingView reference that starts at the range start, 37 of
336 exit comments differ by default, including 5 finite values where
TradingView is na; none differ with the origin declared. Across six corpus
cases, default runs add, drop or shift 1 to 3 trades in the first days; all six
are exact with the declared origin.
This static note does not establish startup adequacy. The selected-window
consumer may show its startup sentence only for a known finite shortfall in
bars of that site's evaluation-context timeframe; absence is not parity.
No sidecar exists yet, at
compile time or at run time: this revision emits no call_site_id and no
startup fact. The subsequent startup inventory and extraction must bind a
versioned sidecar to Pine/generated identities, and share call_site_id and
the exact source range with the diagnostic rather than joining by text; until
that work lands, the static notes remain unconditional. Input-bound lengths retain
the input name or expression; runs/trials resolve submitted values and studies
retain searched ranges. Recursive adequacy and seeding facts remain separate
and may be unknown. The typed consumer fixtures (warmup_consumer.py and
warmup_handoff.json in tests/fixtures/diagnostic_notes/) describe that
later handoff; they are contract examples, not transpiler output.
A code is never removed or reused, and a changed meaning gets a new code
(tests/fixtures/diagnostic_codes_pin.json pins each code's severity,
message and hint templates and argument names; it does not pin
user_message). Since 1.5.0 the user_message templates are checked as
follows. The templates of the eleven migrated codes are fixed string for string
by tests/fixtures/diagnostic_notes/delta.json. Every template is checked for
form: a short, parseable sentence over the entry's own argument names. Each of
the 97 warning twins of an error (PF-W1nnn for PF-E1nnn) has a sentence of
its own, not its error's, that says PineForge warns; the 91 that warn only
inside a switch arm also say that PineForge keeps the arm's lowering, and
PF-W1067 to PF-W1072, which warn everywhere, name no arm. No warning or note
template says that PineForge refuses or rejects something, though one may say
what TradingView refuses or rejects. Beyond these checks a template's exact
wording is not pinned. A text no template
renders carries PF-E0000 / PF-W0000 with the text in args.message (and
args.hint); the test suite refuses it. The message text itself is
unchanged and stays the English rendering; runtime.error text a strategy
authors is not a transpile diagnostic. Since 1.5.0 an uncatalogued note uses
the existing PF-W0000 fallback with its actual note level preserved;
PF-W0000's catalog severity remains warning. The diagnostic level is the
wire authority in that fallback case. No new fallback code is allocated.
scripts/gen_diagnostics_catalog.py --write adds identities for newly
spelled templates and preserves existing pins. Since 1.5.0 new entries require
an authored user_message; the generator reports missing, malformed or
unknown-argument presentation templates rather than copying raw diagnostics.
The eleven reviewed severity pin changes above are explicit fixture-backed
exceptions, not a general permission to regenerate existing pins.
check_support=False on either Python function is experimental. It skips
the support gate; the 1.0 compatibility promise applies with the default
check_support=True. Existing bypass paths can produce C++ that fails to
compile or does not match Pine behavior. Those are defects under the emission
invariant, which applies with either value of check_support; see the
open defects.
There is no installed command-line interface. This package makes no
process-exit-code promises for a CLI, shell wrapper, or gate script. Consumers
should use the Python exception and return-value contract or the JSON
ok/diagnostics contract.
Generated C++ has a separate runtime pairing requirement: from 1.0.0 on,
codegen X.Y.Z supports only engine vX.Y.Z with that release's generated
headers and static library; prerelease tags match exactly. (The 0.x releases
pair by the pineforge-release image's record instead; see the README.)
Regenerate C++ and relink strategy libraries on every pair change.
PF_ABI_VERSION equality alone is insufficient. See
CONTRIBUTING.md.