Skip to content

Latest commit

 

History

History
1023 lines (916 loc) · 67.8 KB

PUBLIC_CONTRACT.md

File metadata and controls

1023 lines (916 loc) · 67.8 KB

Public contract for 1.0

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.

Numeric conversions

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.

Identifiers in generated C++

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.

Value lowering (unreleased)

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_rank length is lowered to the engine's SeriesPercentileNearestRank class, with one buffer per call site. A call inside an if appends only when it runs; no tape pins that conditional shape. PF-E5092 remains the refusal for a series length of ta.sma, and its message names the function and says that TradingView accepts the call. PF-E5009 keeps the plain wording, which makes no claim about TradingView, for every other ta.* 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.rci and the atrPeriod of ta.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) and box(na) produce the same missing handles as typed declarations initialized with na.
  • Literal na in emitted strategy string slots uses the host's empty-string sentinel, including from_entry, id, comment and oca_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 query default_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-E2083 to PF-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 omitted qty keeps engine default sizing.
  • An if or switch value 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, last and get reads of line.all, box.all, label.all and linefill.all borrow live handles in oldest-first order, with the normal array bounds checks. Non-var declaration aliases and for 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/varip aliases retain PF-E5086 until 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 retain PF-E5086; polyline.all raises PF-E5085 pending 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.

Optional compiled execution capabilities

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".

Optional coded run failures

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.

A report belongs to the call that returned it

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.

Optional generated settings extension

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.

Request discovery

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.

Released contract

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.

Python functions

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) -> dict

The 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.

Input manifest and override keys

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.

  • supported is false for 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 an input.enum whose 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 with setting_unsupported. Use supported to determine eligibility rather than options emptiness: every unsupported string input has options: [], with or without options=, and so does a supported input written options=[]. 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_all is "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 native std::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 plain input(close)) publishes its series as default and the nine native sources (close, high, hl2, hlc3, hlcc4, low, ohlc4, open, volume) as options; a plain input(close) keeps its string type.
  • An enum input publishes its Enum.member choices as options in written option order, or enum declaration order when options are omitted. It keeps its Enum.member default, which the receipt encodes as the member's index (map it through the receipt's option_values).
  • A color input (input.color) has type: "color". Its literal-foldable default is an unsigned packed 0xAARRGGBB integer (0 through 4294967295), where AA is opacity: 255 is opaque and 0 is transparent. Named Pine v6 constants, hex color literals, and color.new / color.rgb with literal arguments publish the value the generated get_input_int64 default 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) publishes 0x80F23645 as 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 the string type and Pine-spelling default published through 1.7.0. supported and other input fields, the emitted C++ and the native receipt's storage type (int, 64-bit) and historical kind: "string" are unchanged. An unfoldable color default retains its previous unpublished value (None/JSON null); accepted default expressions are unchanged.
  • A typed numeric input (input.int, .float, .price, .time) publishes numeric default, min, max, step and the numeric choices of an options=[...] dropdown where the receipt's value is a literal: a signed number (-2.5), a named constant (LEN = 14, minval=-RATIO) or timestamp("2024-01-02T00:00:00") of a string literal. A plain input(-5) or input(-2.5) is typed int or float by its literal; these signed forms retain a double getter and a float receipt type. A plain input(5) uses an int getter and receipt. An input.bool default is a boolean when the descriptor lowers it to literal true or false, including an inlined constant; not true remains 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) and not true. Such a default is None and such a min or max is omitted. The whole numeric options field is omitted when any choice is unfoldable. These gaps are pinned in tests/test_input_metadata_receipt.py's KNOWN_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 plain input(...) whose default is not a literal is typed string with a None default.

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++:

  1. 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.
  2. Otherwise, the name of the declaration containing the input call.
  3. 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.

Pyodide and gate/glue JSON

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.

Diagnostic codes

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 string PF-<S><NNNN>. E and W are historical identity prefixes, not the severity: the eleven codes that became notes in 1.5.0 keep their PF-W codes. Its first digit is the area that spells the text: 0 source (lexer, parser, limits), 1 support checker and requests, 2 analysis, 3 request.security, 4 libraries and imports, 5 code generation, 6 array and matrix history, 7 ta.*. The support checker reports what it refuses inside a switch arm as a warning: PF-W1nnn with nnn below 500 is PF-E1nnn there.
  • Severity (Diagnostic.level, the severity of a glue diagnostic and of a catalog entry): "error", "warning" or, since 1.5.0, "note". An error is fatal and raises CompileError. A warning and a note are nonfatal: a script that transpiles with either returns its C++ (ok: true in the glue envelope), and a CompileError can carry them beside its errors. The catalog declares each code's severity and the diagnostic carries it; read it, never infer it from the E / W prefix 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_message and the JSON diagnostic. It uses only the diagnostic's existing args names and may be constant. It is the raw template over args, not pre-rendered English; it does not replace message and hint, and it never substitutes the unbounded raw diagnostic as its whole sentence. For example, "{name}() does nothing in a backtest." travels with args.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.

Compatibility boundary

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.