@tableofcontents
A hand-written C++ strategy runs the standalone native path: one
NativeRunSpec, one working request roster, one physical lot book, and the
host callbacks below. PineScript is not involved at any point — no
strategy.* command, no codegen, no source adapter — and the same kernel
answers a batch, a stream and a C caller.
Subclass pineforge::NativeStrategyHost. Configure with configure_native,
then run or stream_*. Submit from native begin/bar callbacks, or between
realtime inputs on the same thread. Do not override the inherited on_bar
(it is final and refused). Do not write protected engine fields.
The callbacks. Only on_native_bar (native_host.hpp:893) is
pure-virtual: it is the script-bar calculation, and a host that overrides
nothing else is a complete, correct host — every other callback below has a
default that is the established behaviour. The surface is not close-only.
| callback | when | section |
|---|---|---|
prepare_native_begin (native_host.hpp:865) |
once, with the begin's own arguments, before the run starts | Lifecycle and run identity |
on_native_run_begin (native_host.hpp:872) |
once, after the reset, before any bar; the one place declare_timeframe_subscriptions and declare_auxiliary_feed are legal |
Higher-timeframe series for native hosts |
on_native_input (native_host.hpp:875) |
once per accepted confirmed input, before it is aggregated or matched | Calendar, session, timeframes, warmup |
on_native_tick (native_host.hpp:878) |
once per accepted realtime print, before it is matched | Batch OHLCV vs ticks vs quiet |
on_native_timeframe_bar (native_host.hpp:885) |
once per delivered bucket of a declared series | Higher-timeframe series for native hosts |
on_native_bar_open (native_host.hpp:889) |
at the modeled opening, before that point's matching pass | Native requests (and its lookahead warning) |
on_native_bar (native_host.hpp:893) |
the script bar's own calculation | Calculation timing |
on_native_recalculate (native_host.hpp:913) |
every calculation of the run, tagged with its reason; the default forwards to on_native_bar |
Calculation timing |
on_native_sub_bar (native_host.hpp:928) |
after each retained lower-timeframe sub-bar's whole path | Sub-bars |
on_native_applied (native_host.hpp:938) |
after each applied execution — the calculate-on-fill point | Native requests |
on_native_margin_call (native_host.hpp:1001) |
right after the on_native_applied of a kernel liquidation's own fill |
Margin and liquidation |
The answering hooks — each is consulted, and each has a default that is
the kernel's own answer: resolve_execution_terms (native_host.hpp:947),
validate_execution_precommit (native_host.hpp:960),
resolve_margin_requirement (native_host.hpp:976), margin_check_allowed
(native_host.hpp:988), resolve_margin_call_units (native_host.hpp:995),
resolve_anchored_level (native_host.hpp:1012), owns_lot_excursions /
closed_lot_excursion (native_host.hpp:1030) and the hash seam
hash_host_extension. Each is documented beside the feature it shapes.
Declaring a hook the host does not have. Two of those defaults still cost
the kernel work at every bar or fill. A host whose on_native_bar_open does
nothing says so with declare_native_bar_open_hook(false)
(native_host.hpp:1047), and one that keeps the default
validate_execution_precommit with declare_native_precommit_hook(false)
(native_host.hpp:1055). The kernel then makes no bar-open call, and neither
consults the precommit hook nor builds the settlement preview it would have been
shown, unless the host owns lot excursions, which that preview's closing rows
consult. It keeps every effect of its own that the skipped call's boundary has,
so a declaring host's run is its empty-hook run, value for value. A declaration
stands, across runs, until the host makes another. A C host's callback table
declares both (Driving the kernel from C).
Where to go from here. Lifecycle and run identity → NativeRunSpec (price grid, feed policies, the intrabar path, validation) → Native requests (triggers, intents, owners, groups, brackets, trails) → Close execution → Margin and liquidation → Risk limits → Calculation timing → Reading the run back → Reporting for native hosts (the equity curve, the per-bar broker hashes, the closed rows and their close cause) → Calendar, session, timeframes, warmup → Higher-timeframe series, The auxiliary finer feed and Instrument feeds → Batch OHLCV vs ticks vs quiet → the worked C++ example → Sizing without a host override → Examples → Driving the kernel from C → Building the kernel only.
Headers: <pineforge/native_host.hpp>, <pineforge/native_run_spec.hpp>,
<pineforge/native_fx_curve.hpp>, <pineforge/native_order.hpp>,
<pineforge/native_calendar.hpp>,
<pineforge/market_driver.hpp>, <pineforge/order_action.hpp>,
<pineforge/execution.hpp>. Enumeration members live in those headers; this
page does not re-list every enumerator. Request-value members live in
<pineforge/native_order.hpp>.
<pineforge/native_toolkit.hpp> is header-only and additive — the bracket
builder and the id book over the primitives below.
<pineforge/native_module.hpp> is separate: it is needed only to export a host
as a loadable module (see @ref native_engine_examples).
<pineforge/native_c_api.h> is the C host surface: it drives the same kernel
through a callback table, and what it does not spell in 1.0 is the 1.0 C
boundary table (Driving the kernel from C).
Coming from PineScript? [PineScript to native C++](@ref pine_to_native) maps
each strategy.* concept to its native counterpart
(@ref pine_to_native_map), migrates a small strategy end to end
(@ref pine_to_native_worked), and states how to diff a native port against its
Pine twin (@ref pine_to_native_parity).
NativeStrategyHost is noncopyable and nonmovable. The constructor binds the
native consumer; there is no attach/replace switch.
configure_native(const NativeRunSpec&) is the only setup. Observation is
native_state(). Kinds:
| Kind | Meaning |
|---|---|
Unconfigured |
Fresh host. |
Ready |
Spec staged; not begun. |
Running |
Batch, Warmup, or Realtime. |
Completed |
BatchComplete or StreamEnded. Open lots and live requests remain visible, not actionable. |
Failed |
A non-recoverable failure is durable: use a fresh host. A cooperative Aborted failure is recoverable; configure_native may reuse the aborted host with the same session key and a higher run number. |
RunIdentity is (session_key, run_number). session_key is nonempty UTF-8
and becomes fixed on first successful begin. run_number is a positive
integer. A consumed high-water lives outside per-run reset and advances
only at successful begin. Later runs on the same host need a strictly
larger number. A fresh host has high-water zero, so it may replay the same
logical run.
Configure:
Unconfigured→ validate/copy the whole spec atomically →Ready, orFailedwith no partial apply.Readyagain →Failed(Contract). Use a new host to change unconsumed setup.Completed→ same session key,run_numberabove high-water →Ready. Prior-run records stay inspectable until the next successful begin.Running→ refused. A non-abortedFailedhandle keeps its first failure and refuses configure; anAbortedhandle may be reused with the same session key and a higher run number.
Begin (run / stream_begin) is allowed only from Ready. Success consumes
the run number, projects the spec onto execution storage, resets once
(book, live requests, native events, decision floor), enters Running, then
calls on_native_run_begin() and bar callbacks. Reset does not clear the
host high-water.
stream_begin is one logical begin plus internal warmup, then Realtime,
without a second public run and without resetting lots, requests,
events, ordinals, floor, or identity at the handoff.
last_error() is presentation text. native_state().failure is the durable
record (code, operation, optional ordinal, discriminator, and
allocation-free context). Cause/recipient/cursor facts use
NativeInRunCause / NativeInRunRecipient / NativeInRunCursor selected by
NativeFailureContextKind; identifiers belong to the failed spec's
RunIdentity (native_failure_context_in_run, native_failed_run_identity).
A foreign run is dropped, not relabeled. Failure copy/move does not allocate.
See NativeFailureCode / NativeFailureOperation in native_host.hpp.
native_continuation_hash() folds the resolved timezone identity of the run by
its content: the source kind, the effective definition and
TimezoneIdentityDescriptor::resource_digest, an FNV-1a 64 over the bytes of
the zone files the resolver actually read. zoneinfo_root and resource_paths
stay on the descriptor as diagnostics and are not folded, so the same spec over
the same bars answers the same value on a glibc host reading
/usr/share/zoneinfo and on macOS reading
/private/var/db/timezone/tz/2026c.1.0/zoneinfo (R5 lane E23; before it the
paths were folded and no two hosts agreed). What still moves it is a tzdata
release that rewrites the zone's rules — the run then really did read different
rules — so it is an identity to compare, not a source constant: compare it
between runs — to prove that stating a field at its default changes nothing, or
that opting in moves the identity — and pin a constant only where the installed
zone data cannot enter it. For that use native_run_spec_digest(spec)
(native_run_spec.hpp): exactly the fields the consumer folds into the
continuation identity for a run spec and nothing else, seeded as the consumer
seeds them, so two specs with equal digests drive identical continuation
identities on every machine with the same timezone resources. It is not a
continuation hash and is never comparable with one.
NativeRunSpec defaults are incomplete. Empty required strings and zero
capital, point value, or account FX fail validation. There is no
UTC/1-minute/24x7 substitution for a missing native spec.
Required:
identity.session_keynonempty;identity.run_number> 0input_tfandscript_tf(exact literals; see calendar below)tickerid- scheduling
timezone(must resolve; empty is not UTC) initial_capital,point_value,account_fx: finite, strictly positive.price_tick: finite and nonnegative; zero means raw, unquantized prices.account_fxis the pre-first-rate fallback; an optional immutable native FX curve is staged separately below.
Always set, with documented defaults in the header:
ticker,type,currency,basecurrency,description,volumetype(may be empty UTF-8 excepttickerid)session: empty and"24x7"are distinct all-day literalschart_timezone: optional observation metadata; empty stays empty and is not the scheduling calendarslippage_ticks:0..INT_MAX. Buy adds, sell subtractsticks * price_tickonce to form the kernel default price. The terms resolver receives that default; an accepted override is final and is not slipped a second time. No mintick snap.fee_kind/fee_value:Percent,CashPerUnit,CashPerExecution;fee_valuefinite and ≥ 0. A percent fee isabs(units) × price × point_value × account_fx × fee_value / 100. Cash kinds are account currency per unit or per execution.close_execution:NextEligiblePoint(default) orAfterCalculationallowed_open_directions:None,Long,Short,Both(default)report_policy:HostRecorded(default),KernelRecordedorKernelRecordedAtHostMarks;report_open_position_at_end:false(default). See Reporting for native hosts below.calculation:BarClose(default),BarCloseAndFillsorEveryModeledPoint;max_recalculations_per_point:8(default, any value including 0 is legal);open_bar_view:Complete(default) orOpenOnly. See Calculation timing below.
Optional, absent unless set:
quantity_grid: finite > 0; admission only, never resizes quantityquantity_tolerance: finite > 0, in units; two quantities within it are one quantity to the settlement (a close within it of a FIFO boundary ends there, a dust lot a close reaches closes whole). See What a host gets from a quantity below.max_abs_units: finite > 0; opening cap on the resulting bookmax_open_lots: positive; surviving + new lotsinitial_margin_fraction: finite > 0 as a fraction, not a percent. Opening admission only; no maintenance liquidation.margin: the generic per-side broker margin model, including its two money bases (basis,level_base) and its check mode. Mutually exclusive withinitial_margin_fraction(setting both isMarginModelConflict). Its presence is what enables the model at all. See Margin and liquidation below.subscriptions: declared higher-timeframe series of the run's own symbol, or series reading another instrument's installed feed. Empty is the whole default surface; see "Higher-timeframe series for native hosts" below.instrument_feeds: other instruments' own bars, each with its own close, keyed by (instrument, timeframe). Empty is the whole default surface; see "Instrument feeds: another symbol's bars" below.
validate_native_run_spec / normalize_native_run_spec report the first
error field. configure_native copies a candidate, normalizes it, then stages
it atomically. Admitted numeric -0 fee becomes +0; other literals are not
rewritten.
Four spec fields shape how the kernel reads the feed and how it presents a refusal. Each is generic, and all four are folded into the continuation identity unconditionally, including their defaults; changing any of them changes the identity.
slot_label_policy(NativeSlotLabelPolicynative_run_spec.hpp:324) —Canonical(default) requires every confirmed input to carry its calendar slot label (interval.open_ms, or the scheduled clippedeligible_open_ms).FeedTolerantaccepts a provider's own strictly increasing labels instead.LegacyTolerantis the deprecated spelling of the same value and hashes identically.legacy_tolerance(NativeFeedTolerancenative_run_spec.hpp:345) — a bit mask of admission exceptions for a tolerated feed shape, separate from slot labels because a host may want the tolerant price admission and canonical labels.BatchStructuralBarsadmits finite but non-positive OHLC and reads a NaN volume as "activity unavailable";WarmupNonNegativeOHLCadmits finite non-negative interim warmup values, while the final warmup close stays strictly positive.native_feed_tolerance_enabledtests one bit.NativeLegacyToleranceis the deprecated spelling of the type.path_order(NativePathOrdernative_run_spec.hpp:335) —Auto(default) keeps the open-proximity rule that decides whether a modeled bar walks its high or its low first.HighFirst/LowFirststate it, so a replay or a live host does not depend on that inference.abort_reporting(NativeAbortReportingnative_run_spec.hpp:41) —Error(default) presents a cooperative abort as an error diagnostic;Quietpresents it as a status result, for a host that models cancellation itself. It is presentation only: the run still endsFailed(Aborted).
timeframe_undetected is the fifth: a public begin with fewer than two bars
may not establish a timeframe, and setting this flag preserves that state
explicitly instead of inventing a clock literal. It is incompatible with
subscriptions (SubscriptionWithoutTimeframe) and with auxiliary_feed
(AuxiliaryFeedWithoutTimeframe).
NativeRunSpec::intrabar (IntrabarPath native_run_spec.hpp:378) is a
std::variant the spec owns — deliberately a spec value, not a caller borrow,
because matching may need the finer bars later, while sealing an aggregated
script bar:
IntrabarPath::none{}(default) — the confirmed OHLC path only.IntrabarPath::lower_tf{bars, tf, samples, distribution, volume_weighted, volume_weighted_min_samples, volume_weighted_max_samples, sample_eligibility}— a retained finer feed. It is what makeson_native_sub_barreachable, and, matched as continuous segments, what gives the margin model a sample to re-evaluate at instead of a whole-bar waypoint (NativeMarginCheckKind::IntrabarSample, below).IntrabarPath::SampleEligibility::ContinuousSegments(default) keeps continuous matching between generated samples;DistributionSamplesrestricts eligibility to the sample points themselves. Each retained bar is walked through its own four turning points, a repeated one included when a leg has zero length (an open at its own low), and a bar that traded nothing at one price (volume 0, open = high = low = close) is one discrete point, walked once (tests/test_native_intrabar_turning_points.cpp).IntrabarPath::synthesized{…}— the same sampler over the script bar's own OHLC path, with no retained feed. Its eligibility is point-only by construction, so it carries nosample_eligibilitymember and delivers no sub-bars.
is_none(), lower() and synthesized_path() are the accessors. The C
spelling is the PF_NATIVE_SPEC_EXT_INTRABAR block of
pf_native_run_spec_ext_v1, and PF_NATIVE_INTRABAR_LOWER_TF is the only
value that retains a feed. Pinned by the private test test_native_auto_path and
the sub-bar sections of tests/test_native_calc_timing.cpp.
validate_native_run_spec(spec) (native_run_spec.hpp:916) answers a
NativeRunSpecValidation (native_run_spec.hpp:899): a
NativeRunSpecError (native_run_spec.hpp:767) and the
NativeRunSpecField (native_run_spec.hpp:732) it first failed on, with
ok() and an explicit operator bool. normalize_native_run_spec(spec)
(native_run_spec.hpp:926) validates and rewrites the one admitted literal —
a numeric -0 fee becomes +0 — leaving every other literal alone. Neither
allocates on the failure path, neither changes a spec it rejects, and the
field order is deterministic, so a host can report "which field" rather than
"invalid".
configure_native runs both over its own copy and answers a
NativeSetupResult (native_host.hpp:385): a NativeSetupStatus
(native_host.hpp:380, Applied or Failed) beside that same validation.
configure_native_fx_curve answers the curve's counterpart,
NativeFxCurveSetupResult (native_host.hpp:393), carrying a
NativeFxCurveValidation (native_fx_curve.hpp:40) — a
NativeFxCurveError (native_fx_curve.hpp:28: LengthMismatch,
NotStrictlyIncreasing, NotFinitePositive, AllocationFailure,
WrongPhase) and the index of the first bad point. Empty parallel arrays are
valid and clear the curve. validate_native_fx_curve
(native_fx_curve.hpp:48) is the same judgement as a pure query.
native_run_spec_digest(spec) (native_run_spec.hpp:1027) is the portable
constant described under Lifecycle and run identity: exactly the fields the
consumer folds into a run's continuation identity, and nothing else. Each
feature suite pins the refusals of the fields it owns — the
NativeRunSpecError rows of tests/test_native_auxiliary_feed.cpp, for
instance — and the digest's "a field stated at its default moves nothing"
property is pinned beside the feature that added the field
(tests/test_native_calc_timing.cpp, tests/test_native_risk_limits.cpp,
tests/test_native_htf_subscriptions.cpp).
price_grid is an opt-in instrument tick ladder, None by default. None is
exactly the established behaviour: price_tick is only the slippage
multiplier and every booked price is the raw modeled one. The source layer
never sets it, so Pine-compatible runs are unchanged.
QuantizeFills: the kernel rounds the fill basis ontoprice_tickbefore slippage, which is itself a whole number of ticks. A limit order's protection cap moves to the tick on its own side, so limit-or-better survives the grid.QuantizeFillsAndTriggers: a resting trigger is additionally tested against the tick-quantized path instead of the raw one. The order's own level is untouched and stays the modeled price a crossing books. This is the generic mechanism only; TradingView's exact half-tick rule is a source-layer quirk and stays there.grid_rounding:HalfUp(default) is the nearest tick with ties away from zero.Directionalrounds toward the region the order needs — a buy limit down, a sell limit up, a stop and a market fill to the adverse side.price_grid != Nonerequiresprice_tick > 0. A zero tick keeps its documented unquantized meaning instead of silently disabling the grid:configure_nativefails withGridRequiresPriceTickon fieldPriceGrid.- The grid is folded into the run-spec hash only when it is set, so a default spec keeps its established continuation identity.
- The grid shapes the kernel's default resolved price. A host that overrides
the price through
resolve_execution_termsowns that value itself. - Exactness. A ladder price is a fixed point of the grid: a level
bit-identical to either binary64 spelling of its tick —
k * price_tick, or for a decimal tickk / (1 / price_tick), the double a decimal literal parses to — maps to indexkwhatever the quotient's last bit says (the nanotick guard alone misindexes ladder prices from about k = 1.7e7, a five-decimal instrument above 167.82), is booked as presented byQuantizeFills, and is its own limit-or-better cap. TheHalfUptrigger threshold is the exact boundary ofgrid_round_half_up— the last raw price whose nearest tick is still inside the region — not the product(k ± 0.5) * tick: an exact half-tick print rounds away from zero and so lies outside a<=region. The Pine adapter still runs withNone. TradingView quantizes per order kind — stop and limit legs and a trail's activation, with or without a trailing offset (R5 lane E5's tapes, below) and at the placement close too (lane E9), against the tick-quantized bar, a one-shot booking the tick that bar reaches; the trail stop, the running best, stop-limit entries and the calc_on_order_fills cursors against the raw path (engine.hpp, design-stop-tick-rounding and design-trail-activation-tick-bar) — whereas the run spec's grid is one rule for every trigger the matcher tests. A per-kind grid mask would spell that inconsistency into the kernel and is not generic. What was measured as this row's divergence — the trail stop, wherebest - offsetlands one ULP under a ladder point on about 14% of (best, offset) pairs on a two-decimal feed, so the grid fired it a bar before the raw path did — was not the per-kind rule at all but a level the kernel spelled wrong on both paths, and R5 lane E16 closed it: see A trail stop a whole number of ticks away below. The two paths now agree on that row (tests/test_adapter_grid_relower.cpp section 5). - The reached rule. Under
QuantizeFillsAndTriggersthe tick-quantized print is the reached price. The matcher's verdict is authoritative, and the core re-validates every activation — stop, stop-limit, trail arm, trail stop — on the same ladder with the same arithmetic: the consumer handsnative_order::ActivationGridtoprepare_triggerthe way acceptance receivesCommandContext::price_tick, so a hit the matcher reports is never refused. Reached means the level itself (a crossing hit books the level) or a print inside the quantized region: its nearest tick underHalfUp, its enclosing tick on the region's side underDirectional, at or past the level's own ladder point. The activation's recordedreached_priceis that quantized print. A limit has no core re-validation; its gate is limit-or-better on the resolved price, which the grid's cap keeps. One rule for every kind: a trail's running best is the quantized print too (nearest underHalfUp, the favourable enclosing tick underDirectional),best - offsetis tested by the same rule, and a zero-offset ride exits on the first print strictly past the best on the ladder. TradingView's per-kind split (a raw best and a raw trail stop next to quantized stop and limit legs) is not spelled here; the adapter keeps it onNone. The fill books where the mode already says a fill books: the level, or the gapped open, then the fill rounding, thenresolve_execution_terms. Example: a buy stop at 100.50 on a 0.25 ladder with the bar opening at 100.40 activates at the open and books 100.50; underNoneandQuantizeFillsthe raw open never reaches it and nothing fills. - Scope per mode.
NoneandQuantizeFillskeep the raw compare on the raw print for every activation, bit for bit (QuantizeFillsquantizes fills only). OnlyQuantizeFillsAndTriggerstests and re-validates on the ladder. The alternative reading — the raw print stays authoritative and the matcher must not accept a quantized touch at a resting cursor — was rejected: an intrabar crossing already books a quantized touch at the level, so it would make a gapped open second-class for no reason but the core's compare, and it would collapse the trigger mode towardQuantizeFills. A leg born at a bar's calculation is first tested at the next point (its own birth print is excluded), so aclose_execution = AfterCalculationleg reissued at the close and reached by that close's print fills at the next open (the private testtest_native_price_grid, sections L8b-1 to L8b-4). - The Pine adapter's grid re-lowering is waived: measured-infeasible. The
trial has the
adapter submit raw levels (
source_trigger_thresholdanswering the level itself) andproject()declareQuantizeFillsAndTriggerswithHalfUp. The first blocker is closed — no run aborts any more (the first trial measured nine NYSE:F / AAPL zero-offset-trail tapes and sixprocess_orders_on_closepanels failing with "native working-request preparation failed"; under the trialtest_zero_offset_trail_rides_l4cpassed 449 of 450 andtest_pooc_short_close_tick_l4d183 of 183). The second blocker is what is left and it has no adapter-side remedy: TradingView quantizes per order kind, the grid is one rule for the run. The old N13 trial moved 30 pinned checks in four units; that is a historical measurement, not a current-head result. The current native-only ruling keepsNativePriceGrid::Nonefor the adapter. The four units were — 24 intest_coof_market_limit_recross_l4c(an on-grid exit limit stays raw undercalc_on_order_fills), 3 intest_stop_tick_rounding_l4d(a stop booked 14.01 on another bar where TradingView books 13.98), 2 intest_adapter_grid_relower(the trail stop one ULP under its ladder point exits a bar early) and 1 intest_zero_offset_trail_rides_l4c(the quantized running best) — plus 12 checks with an adapter-side cause (the grid pre-roundsdefault_resolved_price, 196.135 booking 196.14 for TradingView's 196.13; a rounded-money liquidation twin; eight pending-book digests where a bit-equal re-issue keeps a request the adapter replaces today). The corpus cannot arbitrate: all 312 probes run a 0.01 tick on an on-grid feed, and under the trial 5 of them differ only in the engine-only entry-incarnation column. No class of triggers is byte-identical on its own, because the grid is a run-wide switch and a per-kind mask is not generic; the adapter stays onNoneand keepssource_trigger_threshold. - Native-only by ruling. The waiver above is final, and it
is not a gap: the grid is a native-host feature whose consumers are native
hosts. ADR-0001 ("Kernel capabilities the Pine adapter does not declare")
records the ruling and
scripts/check_native_feature_rulings.pyholds it: the check fails ifproject()ever starts assigningprice_gridwhile the ruling stands, or if the ruling loses its executed consumers.examples/native/native_price_grid_strategy.cppruns one strategy on one sub-tick tape underNone,QuantizeFillswithHalfUp,QuantizeFillswithDirectionalandQuantizeFillsAndTriggers, and asserts every booked price against a hand-computed one (the same market entry books 100.10, 100.00, 100.25 and 100.00; the breakout stop at 99.75 fills only under the trigger mode, from a raw high of 99.65);native_price_grid_c.cis the same host through the C API, with the same numbers.
Timeframe arguments on run / stream_begin must be omitted/empty or
byte-identical to the spec. Conflicting values are a preflight refusal:
Ready/Running is preserved. Magnifier/source-feed arguments are not native
spec fields.
The rich run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)
overload (engine.hpp:1800-1812) is not refused as a source mutation: it
reaches NativeExecutionConsumer::run_rich
(native_execution_consumer.cpp:10354-10396), which admits the begin, checks the
timeframe arguments against the spec, preflights and pumps the batch exactly
like the plain overload. inputs / syminfo / overrides are carried only as
NativeBeginArgs fields to prepare_native_begin — the overrides as the
opaque overrides_opaque (native_host.hpp:758), which the kernel forwards
and never dereferences. No test pins either behaviour; prefer the plain
overload. What is refused is source mutation through the setters
(below).
From a native callback in Batch / Warmup / Realtime:
submit(request);
replace(handle, request);
cancel(handle);
submit_market(request);
replace_market(handle, request);submit / replace are the complete host commands. submit_market /
replace_market keep the existing market-only call sites and must reject
nondefault trigger, capacity, owner, or group extras rather than drop them.
Serialized external C++ calls may command only between realtime inputs,
never reentrantly during input processing. A host written in C issues the same
five commands through strategy_native_submit_v1 / _replace_v1 /
_cancel_v1 / _cancel_all_v1 / _cancel_where_v1
(native_c_api.h:2829-2917), under the same legality rule; see Driving the
kernel from C below.
native_order::Request values belong to native_order_v7
(native_order.hpp:25); identity types stay native_order_v1. Label/comment
remain inert text. The market default path still constructs from:
order_action::Transact{signed_units}— finite nonzeronative_order::Reduce{native_order::ExplicitUnits{units}}— finite positiveexecution::Flatten{}— quantity-free whole-book close
General requests also select typed Market, Limit, Stop, StopLimit or
Trail triggers; ImmediateRemaining or per-point PointBudget capacity;
and explicit owner/group relationships. Partial executions retain live
remaining quantity. Owner-bound closes use committed opening exposure and
cycle identity through scoped settlement. Group cancellation/reduction is
caused by committed execution events. See the request header for the exact
value types; source-specific Pine lowering remains codegen/adapter work.
A limit is two facts. Its level gates the fill: the modeled path has to reach
it, and a crossing books the level itself while a point already inside the
region (a gap open) books that print. Its bound caps the resolved fill at the
level after slippage. Limit{price, fill_through} keeps the gate and drops
the bound (market-if-touched): slippage, or a host's own
resolve_execution_terms answer, may carry the fill past the level, where a
bounded limit is capped or — for host terms past the level — refused with
InvalidTerms. Without slippage the two book the same fill; the flag is
durable request state and is folded into the identities whether or not it
mattered. Pinned from a bare host, both sides, in
tests/test_native_limit_fill_through.cpp.
At host epoch engine_script_run_v19 (native_host.hpp:20), general requests
also support explicit
native_order::ReverseTo{signed_units} and HostSized. A HostSized{Open, Side} request binds its units at a matching candidate through the host's
resolve_execution_terms override. The host may choose Transact, exact
ReverseTo, or whole-opposite-book close shape there; it does not supply a
second matcher, book, or cash path. submit_market remains the deliberately
narrow market-default convenience surface.
BindOpenings{{first, last}, cycle} on a Flatten or explicit-unit Reduce binds
one fixed cohort of already-live opening provenances. All handles must be
unique, nonzero, in this run and physically live in the supplied positive
cycle. Enrollment rejects the entire request if any member is invalid.
Accepted definitions use canonical handle order; execution still closes lots
in physical FIFO order. Later retirement narrows the live subset, while the
immutable definition retains the original cohort. Several fragments of one
opening share its provenance. A new handle with the same label is excluded.
One selected request produces one execution and ticket, even when it closes
several rows. ExecutionAppliedEvent::scope uses native ExecutionScope
(Book, OpeningExposure, SelectedExposure). Financial execution::CloseScope
remains the original two-alternative type. The event's committed row range
identifies actual contributors to a quantity-limited close.
Inside on_native_bar or on_native_applied, a request accepted or replaced
in that exact callback can be consumed synchronously:
native_order::Request request{native_order::Flatten{}, "close cohort", ""};
request.owner = native_order::BindOpenings{{first, last}, cycle};
auto accepted = submit(request);
if (accepted.handle) {
NativeCurrentExecution command{*accepted.handle, NativeCurrentPriceRule::NearestTick};
auto preview = inspect_current_execution(command);
// The adapter may validate its own policy here. Preview is never an apply token.
if (!preview.refusal && preview.settlement_readiness == execution::Status::Applied) {
auto outcome = execute_current(command);
// Applied effects and relationship drains are visible before this statement.
}
}The command contains only a target and price rule. Membership comes from the
request. It cannot supply another selection, a saved cursor, price, ticket,
or prepared financial plan. Current execution supports Market with
ImmediateRemaining, resolved units, Independent/BindOpening/BindOpenings
ownership and existing groups; current Transact remains Independent.
Priced, budgeted and waiting requests retain queued semantics and receive an
explicit current-execution refusal, and so does a request bound to a host
roster (BindCohort), whose membership is read at the match: it is refused as
UnsupportedRequest and fills at the next match like any queued close
(tests/test_native_current_cohort_refusal.cpp). Only the chosen target is
consumed.
current_execution_point() returns an owning presentation of the active
callback's quote and calendar-derived decision context. AsPresented uses that
price; NearestTick uses the existing rounding primitive. Configured directional
slippage applies once. An ordinary execution notification anchors at its
resolved execution price. A current execution notification inherits its cause's
quote, so a chain does not compound slippage. Finite nonpositive current prices
are permitted for pure closes; opening effects and ordinary matching keep their
positive-price restriction. Native opening admission still decides separately.
NativeCurrentExecutionPreview::settlement_readiness reports the financial
pre-source preparation boundary. It is independent of the account projection's
status and excludes later counter/lifecycle checks and opening admission.
Refusals leave readiness absent. A HostSized preview can instead report typed
terms_rejection or terms_cancellation; those are facts about the proposed
terms, not a saved host verdict. Invalid/NoEffect readiness skips source
preflight. Applied readiness requires source preflight even for opening-only
commands whose account projection overflows. Ordered closed_row_pnl values
exist only for Applied readiness and use execution's pinned ticket allocation.
Execution revalidates; editing the preview cannot authorize or alter a fill.
The default-empty on_native_applied(event, context) notification follows the
Account record and group/owner/dependency drains. Synchronous commands enqueue
notifications FIFO until the outer callback returns and passes the abort check.
Callback depth stays one; event values remain valid during later
submissions. Notifications never automatically execute newborn requests and
have no semantic 64-execution cap. Failure after a physical commit discards the
host; a failed host cannot retry.
The engine fields a run projects from its spec (the capital, point value, FX
scalar and curve, tick, commission and the instrument and zone strings) are
compared with the applied spec before every in-callback execution
(execute_current, whose own precondition it is), at run begin, and at the two
ends of every pump: a batch before its first input and after its last, a stream
at each public input (R5 lane V19-C). Every callback and policy-hook boundary
inside a pump checks the cooperative abort alone. A host that writes a projected
field inside a pump — outside the contract, which forbids writing protected
engine fields — fails with ProjectionMismatch at the pump's end, with the
pump's operation (Input or Stream) and ordinal 0; until then the run goes on
with the configuration it wrote, fills and notifications included.
Generated Pine code runs on this same consumer: source::PineStrategyHost
derives from NativeStrategyHost (pine_strategy_host.hpp:287) and lowers
every strategy.* command into the native requests above. What the source
layer keeps on top of them is TradingView's policy — the command batching,
the priority and activation quirks, the money rounding — never a second
matcher or a second book. The boundary, rule by rule, is
docs/adr/0001-kernel-adapter-boundary.md.
on_native_applied is the calculate-on-fill hook: it is the point at which
a host reacts to its own execution and may submit again. A request born there,
mid-bar on a continuous segment, is eligible on the remaining path suffix of
that segment — the birth is admitted at the current cursor and the geometric
search then sees only the unconsumed suffix (born_on_remaining_path,
native_execution_consumer.cpp:6364-6368). Requests accepted before the
segment, and discrete points, keep the ordinary birth gate above.
on_native_bar_open fires at the modeled opening, before that point's matching
pass (native_execution_consumer.cpp:7882-7884). Lookahead warning: the
Bar it receives is the complete script bar — the consumer has already set
engine.current_bar_ = open_view (native_execution_consumer.cpp:7428), the
complete bar unless the spec asks for NativeOpenBarView::OpenOnly — so its
high, low and close are the finished bar's, not what is known at the open. A
host that must decide on open-only information reads
current_partial_bar() (native_host.hpp:1073; C:
strategy_native_partial_bar_v1), the lookahead-free bar so far (at the bar
open, the open alone), or declares NativeOpenBarView::OpenOnly, which masks this one
callback's bar down to its open. Both are under The bar so far, and the
open-bar view below.
A quantity_grid, when present, admits Transact/Reduce quantities on the
exact binary64 grid in native_order.hpp, and a Reduce whose units are a
FIFO boundary of its scope -- the binary64 sum, in book order, of the scope's
lots through one of them: the head lot's own size, a prefix, the whole scope --
which settlement arithmetic can move off any decimal grid
(CommandContext::units_are_scope_boundary, R5 lane K-ULP4), for a request
that settles in one fill; its candidate settles what then remains only on the
grid, at a boundary of the scope as it stands or at least at its held total,
and refuses anything else with MatchRejectReason::UnrepresentableQuantity.
Flatten is not gridded. Rejection does not rewrite the attempted bits.
Acceptance is not a fill. submit_market returns SubmitResult:
Accepted— timeline ordinal +RequestHandle(session, run, incarnation). Immutable birth is(acceptance_ordinal, decision_floor). Lots and fees do not change.Rejected— rejection ordinal, no handle (InvalidQuantity/OffGrid).
Ordinary queued fills appear later as ExecutionAppliedEvent on the same command history
(native_events(0)). Default market requests retain acceptance/incarnation
priority at an eligible matching driver point. Their birth eligibility is
point_ordinal > birth.acceptance_ordinal and
effective_time_ms >= birth.decision_time_lower_bound (point_eligible in
the header). A request accepted on bar N cannot fill on that bar’s already
delivered opening.
Replace is ordinary and causal: validate first; on success retire that live
incarnation and birth a successor (new handle/priority, predecessor link).
ReplaceRejected leaves the target live. Cancel live → Cancelled. Same-run
absent, replaced, or already terminal → NotWorking. Foreign or malformed
handle → InvalidHandle. Every outcome is an event. Commands never move
lots or cash.
For independent market requests, Reduce/Flatten use current exposure,
not the acceptance-cycle book: a flatten accepted while flat can still close a
same-point opening that already filled. If the book is flat at execution,
Reduce/Flatten terminalize NoEffectEvent: no execution identity, no fill,
no fee, no physical action. MatchRejectedEvent is an event ordinal without
execution identity (opening direction, max units/lots, initial margin,
nonpositive resolved price, or a quantity the settlement cannot book exactly on
the book it met -- UnrepresentableQuantity, which ends that request and not
the run; see What a host gets from a quantity below). An opening denial rejects the entire
Transact, including a proposed close remainder; it does not close old
exposure first. Closing-only Reduce/Flatten remain allowed while opening
limits are already exceeded.
Query the book with physical_position() (signed_units, average_price,
lot_count) and native_marked_equity(mark). native_events(after_ordinal)
returns owning snapshots: command, driver, and account rows. Later
commands/reset do not invalidate copies already returned.
The same book lot by lot is native_open_lots(mark): one
owning NativeOpenLot per open physical lot, oldest first, carrying the
lot's identity (ordinal, entry_incarnation, cycle), its booking facts
(entry_label, entry_comment, entry_time_ms, entry_bar_index,
entry_price, signed_units), the entry fee still on it
(entry_commission) and three fields marked at mark: unrealized_pnl —
the lot's own term of native_marked_equity(mark), fee-net, so the realized
balance plus the rows is the marked equity — and the gross
favorable_excursion / adverse_excursion the kernel has sampled along the
delivered path with mark folded in. It reads only what the book holds and
moves nothing (no fill, no hash, no row); a NaN mark keeps the booking facts
and leaves unrealized_pnl NaN. It is the strategy.opentrades.* surface of
a bare host; the field-by-field map is in
[PineScript to native C++](@ref pine_to_native_map_trades). A kernel
liquidation is seen through it and through nothing else: the whole lot at
the bar's open with the level native_liquidation_price() solved, then —
after the path breached it — an empty book (Flatten) or the same lot,
identity kept, shrunk to the survivor with the level re-solved
(RestoreMinimum), and one closed row under the broker's own ticket with
CloseCause::Liquidation. Pinned by the two liquidation scenarios of
tests/test_native_open_lots.cpp.
A run-start request uses the first provided input's opening time as its initial decision floor. An empty batch has no market time or price to deliver. After the first realtime tick or time-advance call, confirmed-bar input is refused; time advance is likewise refused after confirmed-bar input.
Rows with the same ordinal describe one execution and its account projection. Process the whole ordinal group before advancing an event-reader cursor.
A live request is visible without replaying the event log:
for (const auto& row : native_working_requests()) {
row.definition->request; // the accepted request
row.remaining; // what is left to execute
row.trigger_state; // where its own trigger has reached
}
std::size_t ended = cancel_all(); // one CancelledEvent per request
std::size_t dropped = cancel_where("bracket"); // by the request's comment
std::size_t gone = cancel_where("entry-7", NativeRequestField::Label);native_working_requests() copies owning value rows in live order; later
commands do not invalidate a row already returned. cancel_all() answers how
many requests left the book, dependants of a cancelled owner included;
cancel_where(comment) cancels exactly the live requests carrying that
comment and answers how many of those it cancelled. An unknown comment is not
a command.
NativeRequestField chooses which of the two identity texts the predicate
compares: Comment is the one-argument form, Label addresses the requests
by Request::label — the one call that withdraws every live request a host
issued under one of its own order ids. Both are free host text the kernel
only copies and compares, and neither is indexed: each form walks the live
book once, exactly as cancel_all does, so a label may be reused, replaced
or left empty with no second copy of the book to keep in step. The C spelling
is strategy_native_cancel_where_v1(host, text, PF_NATIVE_FIELD_LABEL); an
id → handle book a host wants to re-price from, rather than a predicate, is
native_toolkit::OrderBook<Key>.
A leg can be placed before its owner has a price. Request::anchor defaults
to Absolute{} — the level written in the trigger. FromOwnerFill{offset, ticks} instead defers it: when the owner's fill arms the request, the level
becomes fill + offset, with offset signed (adverse is negative) and
spelled in price ticks when ticks is set. The anchor applies to Limit and
Stop prices and to a Trail arm threshold. The rule for the anchored
field is that you do not write it: leave it at its own absence — the 0.0
Limit::price and Stop::price already default to, and either an absent
std::optional or that same 0.0 for a Trail's arm_price. Writing any
other level is rejected, because the arm would silently overwrite it.
Market, StopLimit, and any owner other
than WaitForApplied are rejected too, because only that relation arms. The
ArmedEvent carries the materialized definition: from then on the request
reads as the absolute level it now is.
native_order::Request stop{native_order::Reduce{native_order::OwnerOpenedUnits{}}, "sl", ""};
stop.owner = native_order::WaitForApplied{entry};
stop.trigger = native_order::Stop{0.0};
stop.anchor = native_order::FromOwnerFill{-10.0, /*ticks=*/true}; // ten ticks under the fillAn anchored leg is hostable as a first-class bracket child through three
opt-in knobs, each defaulting to the behaviour above, and one measured
fact. The kernel owns the mechanism — the arm, the once-only
materialization, representability, the ArmedEvent, visibility, matching —
and the host supplies policy through a hook, exactly as
resolve_execution_terms owns the fill price.
- Rounding.
FromOwnerFill{offset, ticks, rounding}:roundingis aNativeAnchorRounding—Raw(default:fill + offsetexactly),HalfUp(nearest tick, ties away from zero) orDirectional(toward the region the leg needs, relative to its own trigger kind and side exactly asNativeGridRoundingdocuments: a buy limit down, a sell limit up, a stop the other way; a trail arm threshold is reached from the favourable side and rounds like a limit). The ladder isNativeRunSpec::price_tickand the arithmetic is the price grid's own. A non-Rawrounding needs a positive tick at acceptance (InvalidTriggerotherwise, like a tick spelling). It is a generic instrument grid; a source language's trigger projection is not a kernel option. - The arm hook.
resolve_anchored_level(const NativeAnchoredLevelView&)is consulted exactly once per materialization, before theArmedEventis built. The view is read-only: the owner (handle, its fill's ordinal, the lot it opened, its resolved price and cursor), the leg (handle, side — a closing leg trades against the lot the fill opened — and trigger kind), the resolved offset in price units, the run's price tick andkernel_level, the level the kernel would install after the rounding.nulloptkeeps the kernel level; a returned value is installed, still subject to the kernel's representability check, whose failure is the existingPreparationErrorpath (NonrepresentableQuantity, reported as a settlement failure). A throwing hook latchesCallbackException. TradingView's half-tick arm and its ULP walk are a quirk the Pine adapter applies inside this hook, never a kernel option. - Visibility.
WaitForApplied{parent, visibility}— the one owner relation that arms — takes aNativeArmVisibility:Working(default) orPendingUntilArmed, under which an unarmed leg is not a working order. It is a live, accepted request the whole time; visibility governs enumeration, not addressing. - First match.
WaitForApplied{parent, visibility, first_match}takes aNativeArmFirstMatch. The arm happens inside the owner's fill settlement, at the owner's fill print.AtArmPrint(default) is the established book: the armed leg is a candidate at that very cursor, so a level the fill print already satisfies matches at the print.AfterArmPrintgives it the birth rule of a request submitted from the owner's fill callback: on the driver point that armed it the print is consumed — a level already reached does not match there, a later crossing on that point still does — and from the next driver point on it is an ordinary working request. It governs the level test of a priced trigger; a market trigger has no level. Both are broker models (a contingent child that enters the book after its parent's trade cannot trade on that print; a simulated bracket commonly may), and a leg underAfterArmPrintis, trade for trade, the leg a host would submit fromon_native_applied. - Scope.
WaitForApplied{parent, visibility, first_match, scope}takes aNativeArmScope, which names what an armed closing leg closes.OwnerLot(default) is the established relation: the lot the owner's fill opened, and nothing a later add brings.Bookbinds the leg, at the arm, to the whole position that fill left — itsArmedEventcarries aBookClosefor that cycle and side, bound at the fill's cursor, the very authority anIndependentclose acquires — so a protective leg placed with its entry also covers later adds and settles through the book like any other close. AHostSizedclose may wait for its owner only underBook(the owner-lot relation sizes from the units the owner opened; a book close is sized byresolve_execution_termsat the match); underOwnerLotit is stillInvalidOwner. A waiting transaction closes nothing and must keepOwnerLot. Both fields fold into the continuation identity only when set, each under its own tag, and an unknown value is refused, never read as the default. The C request carries both, aspf_native_request_v1::arm_first_match/arm_scope, and a working row reads them back (pf_native_working_v1). - Facts at the arm (measured).
Reduce{OwnerOpenedUnits{}}is bound at the arm to what the owner opened (QuantityBoundEvent); aScopeFractionwith the defaultAtMatchbasis resolves at the candidate against the owner's lot;PointBudgetis fixed at submit and metered per point after the arm; the group identity is fixed at submit and its effect runs at the filling member's fill, after the common arm; a rejected owner ends its waiting children (OwnerGone). Two rows do not fit a bracket child and are recorded, not repaired: aSizedintent is never a child (InvalidOwner), and aScopeFraction{AtAcceptance}freezes the pre-fill book at submit. NoSizeTime::AtArmexists;OwnerOpenedUnitsalready is the arm-time size.
The arm chronology of one leg:
submit— accepted withWaitauthority (AcceptedEvent). UnderPendingUntilArmedit is absent fromnative_working_requests().- The owner fills — its
ExecutionAppliedEventseeds the arm drain. - Materialization, once:
fill + offset→ the rounding → the hook → representability → the level is written into the leg's trigger and the anchor becomesAbsolute. ArmedEvent— carries the materialized definition; every later reader (the live book, the C working rows, matching) sees that level.- Matching from the owner fill's remaining path on (suffix eligibility): a waiting leg never matches before its arm under either visibility.
What each reader of the live book sees under PendingUntilArmed:
| Reader | Before the arm | From the ArmedEvent on |
|---|---|---|
| matching | never matches (waiting), under either value | matches |
native_working_requests(), C strategy_native_working_* |
hidden | listed |
replace / cancel / trail_state (by handle) |
address it | address it |
cancel_all / cancel_where (comment or label) |
cancel and count it | same |
| reservation / admission / projected-pending | none in the kernel for a resting request (sibling claims filter by reduction scope; admission reads the physical lots) | same |
| report / trade accessors | settled trades only, no request rows | same |
| continuation / broker-state hash | folded (the visibility folds only when set) | same |
| stream mode | the same consumer and book | same |
native_events |
the AcceptedEvent is there; the history is not the working book |
plus the ArmedEvent |
The chosen enumeration semantics are hidden, not flagged: it is the
knob's definition, it is the shadow-row behaviour a broker that shows no
working child before the parent fills needs, and it keeps the C API's
working rows the same enumeration with no layout change. A
native_toolkit::OrderBook key bound to a pending child is re-priced by
handle rather than through the enumeration.
Trailing offsets accept the same two spellings. Trail::ticks is a
TrailTicks{n} offset resolved against the run's price tick at acceptance:
the accepted request carries the resolved price distance and no spelling, so
a tick-spelled trail and its price-spelled equal behave identically bar for
bar. A zero offset is legal and means ride the best: the level is the
running best itself and the exit is the first move strictly past it. Negative
and nonfinite offsets remain rejected, and a tick spelling without a usable
price tick is rejected rather than read as a price.
Trail::best_seed says where the running best STARTS (R5 lane E14). Absent —
every trail before the field — the best is the arm's own print: the arm
threshold's ladder point when a crossing armed it, the first print the trail
sees when it was submitted already armed. Present, it is a floor on that
start: at the arm the best becomes the favourable one of the seed and the arm
print, the higher for a sell trail and the lower for a buy trail. That is the
broker shape of a trail that rides from its activation instead of from the
next print, and the level is the host's own number — absolute (an anchor
moves the arm threshold, not the seed), finite and positive, and put on the
ladder like an observed print when the run declares a price grid. The
activation event still reports the print the arm happened at. The request
digest folds the seed only when it is present, so no established hash moves;
its C spelling is pf_native_request_v1::trail_best_seed.
A trail stop a whole number of ticks away is the ladder point it names
(R5 lane E16). The stop rides offset behind the running best, and a
tick-spelled offset is resolved as ticks * price_tick — exact arithmetic on
inexact numbers, so the level could land one binary64 ULP off the point the
count names: 11.44 - 5 * 0.01 is 11.389999999999998792 against the ladder
point 11.390000000000000568, and a print that IS 11.39 then did not reach a
stop the run put five ticks under 11.44. When the run declares a price tick,
the best is itself a ladder point and the offset is a whole number of ticks,
the level is now derived by index arithmetic and spelled so no ULP hides it
from the side the stop is reached from (native_matching::ladder_trail_stop,
ActivationGrid::ladder_tick). Nothing is rounded ONTO the ladder: a
sub-tick best, a fractional-tick offset and a run with no declared tick keep
the raw subtraction bit for bit, the stop and the running best stay on the
raw path, and it is the level that moves, never the comparison. The core
re-validates an activation through the same geometry, so a level it disagreed
with the matcher on can no longer refuse a hit the matcher booked.
Re-pricing a trail normally restarts it. replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true}) instead carries the
predecessor's live trigger state — a tracking trail's best, an already active
stop — into the successor. Predecessor and successor must hold the same
trigger alternative, and a retained best must still produce a representable
level for the successor's offset; otherwise the replacement is rejected and
the predecessor stays live.
Two more options change what a replacement keeps. keep_handle can also
change matching when a re-priced parent has waiting children: they retain
their owner and may fill, whereas a plain replace ends that ownership.
keep_handle re-prices the request in place: the successor keeps the
predecessor's handle, its place in the chain (no chain-root pair is folded
again), its children and cohort members, and a trail's arm ordinal, and takes
the number a plain successor would have taken as its priority -- queue ties
still rank a re-priced request newest, and the next request is numbered as
after a plain replace. A group member's fill reaches it where the queue holds
it, behind every sibling accepted or re-priced before it, as it reaches a
plain successor: the group's events and receipts come in that order whatever
the handles' numbers (R5 lane K-OCA-KEEP; until then a re-priced member
drained behind a younger sibling failed the run with code 2, discriminator 1,
CoreFailure::InvalidCause). keep_binding carries a close's book binding: at its
next point the successor binds, with no CloseBoundEvent, to exactly the book
close the event would have installed when the position has not moved (taking
the timeline ordinal the event would have taken, so every later ordinal is the
plain replace's), and binds as a plain successor does when it has. Either may
be set alone; neither has a C spelling. The Pine adapter sets keep_binding
on every re-issue: its placement table, bracket families and pending-order
rows name each placement by the incarnation its command was issued, so it
keeps plain handles.
What the Pine adapter takes from this set is the tick spelling and the
anchored child. A source trail_offset is a tick count, so exit() hands the
kernel a TrailTicks and the acceptance path resolves it against the run's
price_tick — the very syminfo.mintick the adapter projects — instead of
multiplying the count by the tick itself. A relative bracket leg — a
strategy.exit(from_entry=…, profit=…, loss=…, trail_points=…) operand
queued while its parent entry has not filled — is the anchored child above:
at the end of the source evaluation in which the queued exit and its live
parent both exist, anchor_relative_exits submits each such leg as the very
request exit() submits at the parent's fill, placed ahead of it: a
HostSized close under WaitForApplied{parent, PendingUntilArmed, AfterArmPrint, Book} with a tick-spelled FromOwnerFill{±ticks, true, Directional} anchor in the exit's own OCA group. The kernel computes the
level (fill ± ticks on the tick ladder); resolve_anchored_level restates
only TradingView's spelling of that ladder point and its trigger projection —
the half-tick threshold of the quantized bar, a raw on-grid limit under
calc_on_order_fills, the one-shot trail touch — through the same two
functions exit() uses. The parent's fill point still runs the source
exit() pipeline, because the leg's reservation, birth reach, L4C policy and
same-bar ordering are facts of that point and have no kernel analogue; it
adopts the armed child instead of submitting whenever that child is the
request it was about to submit: the same trigger bits, the same OCA group, a
host-sized close of the book. A trailing offset of a tick or more is never
anchored: exit() starts that leg's running best at the activation
(Trail::best_seed), the anchored child cannot carry the seed, so
anchorable_relative_exit refuses the definition and the fill point submits
the seeded leg. On the six tapes of that shape
(the private test test_pending_entry_trail_tapes) the engine books TradingView's bar
and price 41 of 41, the quantized arm 28 of 28 and the running best 13 of 13;
the adopted unseeded child had exited 1 to 24 bars late on those 13 (lane
PAR-ORDERS). Because the child is host-sized and
book-scoped, that holds for every quantity the source resolves at the match
(a percentage, a sibling's remainder) and for every book the parent's fill
leaves (flat, a reversal, a later same-id add), under calc_on_order_fills
and process_orders_on_close as well; and because it is AfterArmPrint, a
level the fill print already satisfies behaves as the callback-born leg
does. Otherwise the children are withdrawn before the path resumes and the
fill point submits as it always did. What stays off the kernel path, each
with its measurement (tests/test_adapter_brackets_relower.cpp):
| Shape | {anchored, adopted, withdrawn} |
Why |
|---|---|---|
explicit qty= (rel-qty-explicit) |
{0,0,0} |
exit() stages an explicit quantity per origin (pending_bracket_legs_) and submits it at a later flush: the fill point has no request for a child to be |
strategy.cancel(exit id) in the same evaluation (rel-cancel-exit-id) |
{0,0,0} |
the definition is gone before the evaluation ends: no leg exists |
an exit issued before its entry call (rel-exit-before-entry) |
{0,0,0} |
the call is void while its entry neither exists nor rests (TradingView's rule, the private fixture set exit_queue), so no leg waits for the entry's fill |
close_entries_rule = "ANY", stream runs |
not anchored | the fill-point leg is cohort-bound (BindCohort), a scope the arm does not spell; a stream's fill point stages the leg |
a trailing offset of a tick or more (rel-trail-offset, rel-short-trail, mag-rel-trail-offset, coof-rel-trail-offset) |
{0,0,0} |
exit() starts the leg's running best at the activation (Trail::best_seed, TradingView's start); the kernel's anchor installs the arm level alone, so the child cannot carry the seed |
a parent re-issued, cancelled or declined (rel-limit-parent {12,4,8}, rel-reissue-changed {18,2,16}, rel-breakout-pair {40,14,24}, rel-declined {2,0,2}) |
withdrawn | the kernel ends a waiting child with its parent (OwnerGone), a replaced parent included; keeping the children across a replace needs a re-parent transition in the event log, which is an epoch decision |
a sibling with no capacity left (rel-two-exits {4,2,2}), a level below zero (rel-negative-short {2,1,1}) |
withdrawn | the fill point submits nothing for that leg |
Over the 49 pending-parent scenarios the suite pins, the split is
{212, 115, 93}, and every shape but the four {0,0,0} rows above runs on
the kernel. The queued definition and its
shadow row stay: the definition outlives parents (it may be declared while
its entry still rests, before any fill, and is re-anchored when a parent is replaced) and is what
the fallback re-runs, and the shadow row is the source projection of a child
the kernel deliberately does not list. The remaining TradingView rules stay adapter
policy because no kernel primitive expresses them: the legacy broker rides
the tick-quantized running best, which the adapter still spells as a
half-a-tick trailing distance (TrailTicks{0.5}) rather than the kernel's
raw-best zero offset, and an offset-only trail re-issue keeps the live
request rather than replacing it, so ReplaceOptions{retain_trigger_state}
has no adapter consumer. See docs/design/native-feature-parity.md §1.2 B3
and A.6.
include/pineforge/native_toolkit.hpp is header-only, Pine-free and additive:
it composes the primitives above and adds no kernel behaviour. A Pine
strategy.exit("x", from_entry="e", limit=…, stop=…, trail_points=…) maps
onto one call:
#include <pineforge/native_toolkit.hpp>
namespace tk = pineforge::native_toolkit;
tk::BracketSpec bracket;
bracket.parent = entry; // the handle strategy.entry returned
bracket.take_profit = take_profit_request; // limit leg
bracket.stop_loss = stop_loss_request; // stop leg
bracket.trail = trail_request; // optional trailing leg
const tk::BracketReceipt legs = tk::submit_bracket(*this, bracket);
if (!legs.every_requested_leg_accepted()) { /* a leg this entry needs is not resting */ }Each present leg is submitted as the parent's WaitForApplied child in one
OCA group — group id the parent's incarnation, one cohort per leg, the
caller's sibling_effect (default Cancel) — in take-profit, stop-loss,
trail order. That is exactly the owner/group shape a host would write by
hand, and the legs' own intent, trigger and anchor are left untouched. A leg
the kernel refuses does not stop the others: the remaining legs are still
submitted.
The receipt says what became of every leg, and nothing about a bracket is
silent. legs.take_profit / .stop_loss / .trail are the handles the legs
rest under and are empty otherwise, so beside each one the receipt carries a
BracketLegOutcome — legs.outcome(tk::BracketLeg::StopLoss), or the
*_outcome member — whose state is one of four:
BracketLegState |
what it means | handle() |
reason() |
|---|---|---|---|
NotRequested |
the spec left the leg unset; nothing was submitted | — | — |
NotSubmitted |
the leg was asked for, but spec.parent was never allocated, so the builder sent nothing and the kernel gave no verdict |
— | — |
Accepted |
the kernel accepted it | the handle | — |
Rejected |
the kernel refused it | — | its RequestRejectReason |
BracketLegOutcome::result is the kernel's own SubmitResult verbatim —
status, event ordinal, handle, reason — so the toolkit spells a refusal in no
vocabulary of its own; state is that status widened by the two cases in
which no submit happened. every_requested_leg_accepted() is the one
question a host must ask before it treats the bracket as placed: reading only
the optional handles cannot tell a leg that was never asked for from one the
kernel refused, and a refused exit leg that goes unread leaves a position
running without the protection its strategy believes it placed.
examples/native/native_bracket_strategy.cpp reads and asserts every leg.
The toolkit is C++-only and has no C spelling. It is header-only composition
over the public request surface — std::optional, the request variants, a
class template — that emits no library symbol and adds no kernel behaviour,
so there is nothing for native_c_api.h to export. A C host composes the
same bracket from the primitives the C API already carries:
strategy_native_submit_v1 per leg with PF_NATIVE_OWNER_WAIT_FOR_APPLIED
and PF_NATIVE_GROUP_MEMBER, which returns PF_NATIVE_E_REJECTED with the
same RequestRejectReason written out — the refusal this receipt surfaces in
C++ is already unmissable there, one call at a time.
Two more knobs cover anchored legs, both defaulting to "leave the leg as
written": bracket.anchor_rounding (an optional NativeAnchorRounding,
written onto every present leg whose anchor is FromOwnerFill) and
bracket.visibility (a NativeArmVisibility, written into the owner
relation the builder creates). A strategy.exit(from_entry="e", profit=…, loss=…) therefore becomes tick-spelled FromOwnerFill legs with
Directional rounding and PendingUntilArmed, and the host's own trigger
projection, if it has one, lives in resolve_anchored_level.
examples/native/native_bracket_strategy.cpp is that shape end to end.
tk::OrderBook<Key> is the id bookkeeping a strategy would otherwise write
itself: submit_or_replace(key, request) re-prices the key's own request
while it is still working and submits a fresh one when it is gone,
cancel(key) ends it and forgets the key, and handle(key) reports the
current handle. An unknown key never reaches the host.
Those two commands answer one value each, and the book is as honest about
them as the receipt is about a leg. submit_or_replace can issue two
kernel commands for one call — a replace the kernel answers NotWorking,
then a fresh submit — so its optional handle names neither: a present handle
is a re-pricing in place or a brand new request born behind everything
already resting, and an empty one is a refused replacement (the previous
request untouched and still bound to the key) or a refused submit (the key
unbound), with the kernel's RequestRejectReason and event ordinal dropped
in both. submit_or_replace_outcome(key, request) is the same call, same
commands, same book state, reporting an OrderBookOutcome:
OrderBookAction |
what the book did | handle() |
reason() |
|---|---|---|---|
Replaced |
ReplaceStatus::Replaced: the key's own request was amended, nothing was submitted |
the successor | — |
ReplaceRejected |
ReplaceStatus::ReplaceRejected: nothing was submitted, the previous request is still working and still bound |
— | its RequestRejectReason |
SubmitAccepted |
SubmitStatus::Accepted for a fresh submit; the key holds the new handle |
the handle | — |
SubmitRejected |
SubmitStatus::Rejected for it; the key is left unbound |
— | its RequestRejectReason |
OrderBookOutcome::replace and ::submit are the kernel's own
ReplaceResult and SubmitResult verbatim and whole — status, event
ordinal, successor/handle, reason — each present exactly when that command
reached the kernel, so a replace the book abandoned as NotWorking before
submitting fresh is reported too, and the toolkit spells a refusal in no
vocabulary of its own. action is the one word it adds: the disjoint union
of the two kernel statuses restricted to the four the book stops on.
submit_or_replace(...) is submit_or_replace_outcome(...).handle(),
exactly.
cancel_outcome(key) does the same for the withdrawal. cancel(key) answers
CancelStatus::NotWorking for three different things — the kernel's own
verdict, a key that was never in the book, and a key whose request had
already left the working enumeration — and the last two reach no host command
at all. The outcome carries known (the key was bound when the call arrived)
beside result, the kernel's own CancelResult, present exactly when a
cancel reached the kernel; cancel(key) is cancel_outcome(key).status(),
exactly. examples/native/native_bracket_strategy.cpp reads a key through
all four actions and both cancel answers.
NativeCloseExecution::NextEligiblePoint (default): a request born at a
script calculation waits for a later eligible matching point (next modeled
opening, observed print, or carried open). It does not fill on the same bar’s
already presented open/high/low/close.
NativeCloseExecution::AfterCalculation: after that calculation, a modeled
close point may match, still obeying birth ordinal/floor. It is not a replay
of observed prints.
NativeRunSpec::margin is the whole generic broker margin model, and it is
opt-in. A spec that leaves it unset keeps initial_margin_fraction's
one-scalar opening gate, never enters a liquidation path, and folds nothing
new into the continuation digest. The two spellings are mutually exclusive:
declaring both fails validation with MarginModelConflict.
NativeMarginModel margin;
margin.initial_long = 0.5; // fractions, not percents; >= 0, 0 = host's
margin.initial_short = 0.5;
margin.maintenance_long = 0.25; // absent = that side never liquidates
margin.maintenance_short = 0.25;
margin.sizing = NativeLiquidationSizing::RestoreMinimum;
margin.shortfall_multiple = 1.0; // used by ShortfallMultiple
margin.liquidation_min_units = 1.0; // optional broker minimum trade
margin.check = NativeLiquidationCheck::PathAdverseExtreme;
margin.basis = NativeMarginEquityBasis::MarkedEquity; // policy, below
margin.level_base = NativeLiquidationLevelBase::MarkedEquity; // policy, below
margin.liquidation_label = ""; // empty = "__kernel_liquidation__"
margin.liquidation_comment = ""; // empty = "Margin liquidation"
spec.margin = margin;The kernel owns the margin mechanism: the level solve, the check points,
the kernel-originated request, its Superseded re-pricing, the receipt and
on_native_margin_call. It exposes the policy through the run spec's two
money bases and three host hooks, the same way resolve_execution_terms
exposes the fill price. Everything below is opt-in and every default
reproduces the pre-policy model exactly, digest included: the two bases fold
into the run-spec digest only once one of them is moved off MarkedEquity.
Whether the model runs at all is the presence of spec.margin. There is
no separate enable flag: a spec without a model never solves a level, never
reaches a check point and never issues a request, and a spec with one is
always live. A host that toggles its broker's margin call at runtime expresses
that as policy — margin_check_allowed() answering false — and not by
mutating the run spec mid-run.
Opening admission. With a model set, initial_long / initial_short
replace the single scalar for that run: an opening is refused with
MatchRejectReason::InitialMargin when
resulting_abs_notional × initial_<side> > marked_equity − ticket. A host
that answers AdmitWithHostMargin from validate_execution_precommit still
takes that one check over, exactly as before. The comparison's FX-bearing
terms — the notional, the marked equity and a percent-fee ticket — convert at
the rate of the gate's own point, as every check point below does: the
candidate gate's matching cursor, and for the placement gate the acceptance
point a Sized{SizeTime::AtAcceptance} froze its units at, never the later bar
clock an applied callback drained at its bar's calculation is presented. The
freeze takes its equity basis at that same point
(tests/test_native_margin_fx_clock.cpp, section 8).
Opening admission and liquidation are two separate broker functions, and a
side may declare either without the other. initial_<side> == 0.0 is the
maintenance-only spelling: the kernel enforces no opening requirement on that
side and the HOST owns opening admission there, while the kernel keeps the
side's liquidation mechanism in full. This matters because the opening gate
has two sites, not one — the candidate gate, where a host opts out per
opening by answering AdmitWithHostMargin, and the placement gate that
freezes a Sized{SizeTime::AtAcceptance} quantity, where no candidate verdict
exists yet. A zero initial is the only spelling that covers both, so it is the
one a host with its own pre-trade money rule uses; the Pine adapter is exactly
that host, and declares its TradingView margin percents as maintenance only.
A side may waive either function but not both: a zero initial_<side> is
legal only where that side's maintenance_<side> is set, and a side with
neither is refused at configure with MarginSideUndeclared. A model whose
initial_* are positive is unchanged, bit for bit.
The liquidation level. With a maintenance fraction for the live side, the kernel solves the one price where the marked equity meets the maintenance requirement:
equity(P) = capital + realized − open entry fees + dir × (P × Q − Σ qty×price) × pv × fx
required(P) = Q × P × pv × fx × maintenance
L : equity(L) == required(L)
fx is the rate of the CHECK POINT being taken, not of the bar the engine
happens to be presenting: account_currency_fx_at(cursor.point .effective_time_ms), what the declared NativeFxCurve has in force at that
point's own instant, on the curve's own step semantics — a step is in force
from its timestamp onward, and the run's scalar account_fx holds before the
first one. It is the rate BOTH terms convert at, the requirement and the
marked equity it is compared with, and the rate L is solved at; at every
check kind (BarOpen, AfterApplied, Calculation, FxRoll,
IntrabarSample), in batch and
in stream alike. A run that declares no curve has one rate at every instant,
so the rule is inert there by construction.
The presented bar clock is a LATER instant whenever the walk has moved past
the point being checked — an applied fill drained at its script bar's
calculation is presented that bar's close coordinate while its cursor still
stands on the opening print it filled on — and converting there calls a margin
the account never owed (tests/test_native_margin_fx_clock.cpp). The rest of
the kernel already converts at the cursor: a Sized request freezes its units
at the acceptance coordinate's rate, and an execution term records its
active_fx at its own cursor. A current execution does too: since R5 lane
B-ADAPTER, execute_current converts at its cursor's rate, threaded through
its terms facts, inspection, preview and settlement contexts, and it neither
writes nor restores the engine's presented clock, so its own hooks and the
host after it see the frame's clock. Its preview, inspect_current_execution,
still converts at the presented clock's rate, so the preview and the
execution disagree only when an FX step falls between the presented clock and
the cursor. The host queries are the exception, and they
are the rule restated: native_liquidation_price(), native_open_lots(mark)
and native_marked_equity(mark) have no cursor of their own, so they answer at
the rate of the instant the engine is presenting. So do the kernel's own
run-end rows (record_open_position_report_rows): they convert at the rate of
the run's last presented instant, while the shared producer they call,
append_open_position_report_rows, converts at whatever rate its caller hands
it — the Pine source layer hands it the rate of TradingView's range-end mark.
Both sides are affine in P, so L is unique unless the slopes coincide —
maintenance == 1.0 on a LONG, where equity and requirement move together and
no price solves the breach. NativeStrategyHost::native_liquidation_price()
answers L, or nullopt when the run declares no model, the side has no
maintenance fraction, the book is flat, or no finite price solves it. It is
the exact level: no tick rounding, which is a source-layer spelling.
level_base chooses the base that solve starts from. MarkedEquity (the
default) is the intercept of equity(P) above — capital plus realized net
profit, less the open entries' commission. RealizedOnly drops that last
term, for a broker whose liquidation level does not answer for costs the open
position has already paid. The two agree whenever no open entry paid one.
Arming (PathAdverseExtreme, the default). At every script-bar open and
after every applied fill, the kernel measures the requirement against the most
adverse price the modeled script path still reaches — the same sizing mark a
whole-bar broker check would use. At the open that is the rest of the bar.
After a fill it is the rest of the segment the fill was matched on and the
waypoints after it: a request matched at a driver point was reached on the
segment into it, so a limit filled on its way down to the bar's low still
faces that low (a short filled on its way up, the high), while the waypoints
the path already passed do not count (R5 lane PAR-MARGIN-2;
tests/test_native_margin_post_fill_path.cpp; TradingView books those calls
on the fill's bar, private tapes pm2-m7-lim-* of margin_entry_bar, -slim-*,
-s1lim-*). If that
mark breaches, the kernel rests its own Reduce (or Flatten) with
Stop{L}, bound to the live book. The reduction therefore fills at the
liquidation level, where the account actually runs out of margin, while it is
sized at the adverse mark. With a declared IntrabarPath there is no
whole-bar waypoint model. On a path matched as continuous segments (a
lower_tf path under ContinuousSegments) the kernel re-evaluates at each
delivered sample: the bar's first sample is its BarOpen point, and every
later one is an IntrabarSample point, measured at that sample's own price
immediately before the sample is matched, so a breach inside the bar rests its
reduction at the sample that crossed, and the path reaches it there
(tests/test_native_margin_intrabar_samples.cpp). A one-price distribution
path (synthesized, or DistributionSamples) is checked at its first sample
and after fills only: a request armed at a discrete point is matched only at a
later one, so a reduction re-armed at every sample would never be. The same
money under the magnifier on TradingView books its call at the first
lower-timeframe low that crosses, sized there
(the private fixture set intrabar_margin). TradingView's lower timeframe is its own:
on a 15-minute chart its bar magnifier walks 2-minute intrabars, each owned by
the chart bar that holds its last minute, so the samples a run declares decide
where its calls land. A compiled Pine strategy declares exactly those: its host
builds TradingView's intrabars from the finer feed (@ref magnifier_intrabars).
basis chooses the equity side of that comparison, at every check point.
MarkedEquity (the default) is marked_equity(mark) itself, which the open
entries' commission has already reduced. MarkedEquityBeforeOpenCommission is
the same mark-to-market equity taken before that reduction, for a broker that
does not charge a still-open entry's fee against the margin equity. It is one
term of one comparison: no cash is booked differently, and marked_equity()
itself does not move.
Arming (PathAdverseExtremeMark). The period-mark broker. It measures the
same breach at the same adverse mark, and rests the reduction at that mark
rather than at a solved level, so the fill belongs to the adverse waypoint the
breach was measured at. It never solves a level, which makes it the one mode
that still checks where no level exists — a LONG at maintenance == 1.0,
where PathAdverseExtreme rests nothing because there is no price to rest at.
The resting price is that fill's default resolved price, so a host that needs
a tick ladder or an exit-side slippage on the forced price applies it in
resolve_execution_terms like any other fill.
The units come from sizing:
sizing |
units |
|---|---|
RestoreMinimum |
(required(mark) − equity(mark)) / (mark × pv × fx × maintenance) — the fewest units that restore the requirement |
ShortfallMultiple |
that restore × shortfall_multiple |
Flatten |
the whole position |
The result is capped at the held quantity, floored onto quantity_grid when
one is configured, and replaced by a full flatten when liquidation_min_units
is set and the computed slice falls below it. A host override of
resolve_margin_call_units(const NativeMarginCallView&) has the last word and
is clamped into (0, held].
Re-pricing. Exactly one kernel liquidation is live at a time. When the
level or the units move — typically after a host fill changes the book — the
previous one is withdrawn with CancelReason::Superseded before the new one
is accepted. A book that is flat or no longer breached withdraws it outright.
CalculationOnly. The kernel rests nothing and tests the requirement only
at a script calculation point, against that bar's close. A breach there is
liquidated immediately as a current execution. Nothing fills mid-path.
The FX roll (NativeMarginCheckKind::FxRoll). required(P) carries the
account rate, so under a declared NativeFxCurve an account can cross its
maintenance line while every price stands still. A step of the curve is
therefore a check point of its own: the first walked driver point the account
converts at a different rate than the previous walked point, offered immediately
before that point is matched. The walk has not moved yet — a discrete point is
measured at its own price, a segment at its origin with its destination among
the waypoints that remain — so the mark is the unchanged price and only the
rate is new. From there it is an ordinary path check: the level is re-solved
at the new rate and the reduction is rested, re-priced or withdrawn. A level
the new rate moved inside the segment that remains is reached by that same
segment and fills AT the level. A level already through the standing price is
treated like any slice rested mid-path: it is not owed the print its birth
segment has consumed and takes the next one. With the confirmed OHLC path, a
curve point stamped at a script bar's open activates at the PREVIOUS bar's
Close coordinate, whose effective time is that same instant — the roll the bar
open used to see one point late, after the Close segment had run under the new
rate against a level solved under the old one. A curve point that restates the
running rate is not a step; a run without a curve has no such point; and
CalculationOnly, which measures at its calculation alone, is not offered it
— its next calculation already converts at the new rate. The detection reads
the driver log and the immutable curve, so it adds no run state and moves no
continuation identity (tests/test_native_margin_fx_roll.cpp). It is stated
in the same cursor time every check is taken in — a point's rate is
account_currency_fx_at(its own effective time) — so "the rate the next match
will use differs from the one the last point used" and "each point converts at
its own instant" are one rule, not a special case beside one.
The requirement hook. At EVERY kernel check point, BEFORE the breach test, the host is offered the two numbers the kernel is about to compare:
struct NativeMarginRequirementView {
NativeMarginCheckKind kind; // BarOpen | AfterApplied | Calculation | FxRoll
// | IntrabarSample
NativePhysicalPosition position;
double mark, equity, required; // equity on the model's basis
native_order::MatchCursor cursor;
};
struct NativeMarginDecision { double required, equity; bool force_breach; };
virtual std::optional<NativeMarginDecision> resolve_margin_requirement(
const NativeMarginRequirementView&) const; // nullopt = the kernel's ownA returned decision replaces both numbers for that check point only, so a host
may raise a call the kernel would not make — a requirement kept to a
broker's own money precision, an equity its account model computes
differently — or veto one it would. force_breach proceeds past
required > equity even when the answered numbers do not meet it; the sizing
policy then runs as usual, and where the answered numbers leave no restore of
their own, resolve_margin_call_units is the whole sizing authority. Neither
answer moves the mechanism: the kernel still evaluates, schedules, places,
books and reports. Source-language money quirks — TradingView's
ten-significant-digit rounding, for one — live in this hook, never in the run
spec.
The check gate. Each kernel check point is offered first:
struct NativeMarginCheckPoint {
NativeMarginCheckKind kind;
NativePhysicalPosition position;
double mark;
native_order::MatchCursor cursor; // cursor.point.path_phase is the waypoint
bool liquidation_resting; // a slice is live from an earlier point
};
virtual bool margin_check_allowed(const NativeMarginCheckPoint&) const; // trueAnswering false suppresses the whole point: nothing is measured, the
requirement hook is not reached, and nothing is re-armed or withdrawn — the
margin state stays exactly as the last admitted point left it. A broker model
whose own schedule is not the kernel's expresses that here. liquidation_resting
is what lets such a model tell its two cases apart: a point that only exists to
RE-SIZE a slice already resting — because the book it was sized on has since
shrunk — from a point that would take a new one. A host that admits every point
(the default) never needs to look. Every point the
run's check mode reaches is offered, the ones where the book is flat or the
live side has no maintenance fraction included, because withdrawing a resting
liquidation is part of the check; CalculationOnly, which rests nothing,
offers only the points it could act on.
Events and hooks. A kernel-issued request carries
RequestDefinition::origin == RequestOrigin::KernelLiquidation; every host
request is RequestOrigin::Host. When it fills, a MarginCallEvent joins the
command history directly after its own ExecutionAppliedEvent, carrying the
mark, the equity on the model's basis and the requirement at that mark, the
solved liquidation price, the filled units and the signed book on either
side. The host sees
on_native_applied first and on_native_margin_call immediately after, with
the same cursor.
Naming the ticket. liquidation_label and liquidation_comment are the
ticket the kernel books its own liquidation under. Left empty they are the
kernel's own "__kernel_liquidation__" / "Margin liquidation"; set, they are
used verbatim, and they reach the closed row's exit id and comment. That
matters because the exit id is how a reporting layer that reads tickets says
"this row was a forced liquidation", and every broker spells its own.
pineforge's own close_cause does not decode it: the settling fill records
Liquidation on the row, which the C ABI answers as 3 MARGIN_CALL (Why a
row closed). Each
folds into the model's digest only when set, so a model that does not name its
ticket digests as it did before they existed
(tests/test_native_margin_kernel_r5.cpp).
The TradingView margin call is this model. The Pine adapter sets margin
from strategy(margin_long=, margin_short=) — the two percents are the
maintenance fractions — with PathAdverseExtremeMark, the equity basis its
commission type implies, RealizedOnly for the reported level and
"__margin_call__" / "Margin call" for the ticket. It declares no sizing
knob: resolve_margin_call_units answers every call the kernel makes on its
behalf, so sizing, shortfall_multiple and liquidation_min_units would be
set only to be shadowed, and TradingView's slice — the restore lot-floored
before the 4×, floored again, and the one-contract whole-drop band for a
sub-lot restore — is not a generic policy the kernel could spell
(scripts/check_adapter_spec_shadowing.py fails the gate if a shadowed field
is ever declared again — a source guard of every ci_verify.py profile and of
ci_preflight.py, before anything is configured, as well as a CTest row —
and the private test test_adapter_margin_relower MG-F3 pins
the gridded case where the two orders of floor and multiple part: 4 lots, not
5). The kernel then solves,
schedules, places, re-prices, books and reports every resting liquidation; the
adapter answers margin_check_allowed with TradingView's scheduling — which
includes the post-exit re-size: when a priced bracket leg of the script bar
fills, the slice resting at that bar's adverse extreme was sized on the
pre-exit book, and the legacy broker cancelled and re-scheduled it there
(margin_check_allowed pine_adapter.cpp:17595-17628), so the adapter admits
the kernel's own point for that driver point while (and only while) a slice
rests —
resolve_margin_requirement with its ten-significant-digit money and
fee-adjusted equity, resolve_margin_call_units with its lot-floored 4×
restore and whole-drop band, and resolve_execution_terms with its
tick-quantized fill price. The presence of margin IS
set_margin_call_enabled(). What stays adapter-side is what no check point of
the kernel's can reach: the bar-open mark checkpoint and the script-close pass
(the kernel checks once per point, at the remaining path's adverse mark), the
account-currency FX rollover revaluation (the adapter refuses the kernel's
FxRoll point by kind: TradingView's rollover is its broker-open slice, taken
on the source's own sub-bar rate), and the
one-contract 1×-long money call — which is not a maintenance liquidation
at all and fires on the favorable side of the path. The pre-open admission
slice stays adapter-side as well, though the kernel's AfterApplied point
after the open fill does reach its bar. Measured against the
adapter as it stood before the re-lowering, on the same books, bit for bit:
the private test test_adapter_margin_relower.
Measured against the kernel's own points (R5 lane H-MEASURE,
the private test test_adapter_margin_schedule_differential): the same bars and
orders through the adapter and through a bare host whose hooks answer
TradingView's money and slice, so that only the kernel's own scheduling,
check kinds and opening gate differ. A gap-open breach books the adapter's
market execution at the open, sized on the open's money; the kernel's mark
check rests its slice at the remaining path's adverse extreme and
CalculationOnly executes at the calculation, so no kernel check kind books
that row. On the same money the pre-open slice and the kernel's
AfterApplied point part on the one-contract band and on the restore's
+1e-6 floor. They parted on the frozen units too until R5 lane PAR-MARGIN:
a default-percent stop entry above 100 % was frozen at its signal but sized
again at its fill, as ab9714be did. Eight TradingView tapes
(private tapes pm-m10-* of margin_entry_bar) show TradingView freezes it at
the snapped stop level, or at the signal close when the stop was already
marketable, whatever the percentage, and books its margin calls on that
book, so the adapter now does too. The opening gate parts both ways:
the kernel declines a 1× long whose entry fee the adapter admits, and admits a
gap-up add the adapter refuses against its signal-time equity. A leveraged
opening (margin below 100 %) is checked on its own entry bar: four TradingView
tapes (the private fixture set margin_entry_bar) book TradingView's margin call on
that bar at its low, as the kernel's AfterApplied point does, and since R5
lane PAR-MARGIN the adapter admits that point for a leveraged opening's entry
bar and books the tapes' rows (before it, the call came a bar late or not at
all). Since R5 lane PAR-MARGIN-2 the admission also covers a limit opening
filled on its way down to the bar's low, process_orders_on_close and
calc_on_order_fills runs, and an add to a book carried in from an earlier
bar, on the combined book: twenty more tapes book the call on that bar at its
low (the private fixture set margin_entry_bar: pm2-m7-coof-*, pm2-m7-lim-*,
pm2-m7-poocl-*, pm2-m7b-*). A magnified or timestamped-FX run keeps
its own route, and a default-percent stop entry taken at its bar's open keeps
the pre-open slice's verdict. Under the bar magnifier the adapter admits the
kernel's IntrabarSample points: TradingView's magnified broker books its call
at the first lower-timeframe low that crosses and again at each later one that
crosses the reduced book's line, on a leveraged long (plain, under
process_orders_on_close or under calc_on_order_fills) and on a short at any
margin, plain or under calc_on_order_fills (the private fixture set intrabar_margin and
private tapes pm2-m7-mag-* of margin_entry_bar, 20 magnified tapes). It checks at its own 2-minute intrabars (above), which
TradingView's request.security_lower_tf prints. Since R5 lane MAG-INTRABAR
the adapter walks those intrabars, built from its host's finer feed
(@ref magnifier_intrabars), and books all 20 tapes' calls; walking the feed's
own minutes, it parted on six where the two grids resolve a crossing
differently. A full-margin long (its one-contract money call) and a short under
process_orders_on_close keep their routes there. A margin call is a market
execution at the print it fired at, so an off-grid print books its nearest
tick (12.105 books 12.11), as every TradingView market fill does, while a stop
or limit crossed at its own off-grid level keeps its directional tick
(the private fixture set half_tick_rounding).
NativeRunSpec::risk is the generic account-risk block, and it is opt-in. A
spec that leaves it unset measures nothing, appends no event, refuses no
opening and folds nothing new into the continuation digest. The existing
per-opening caps — max_abs_units, max_open_lots,
allowed_open_directions — are unchanged and independent of it.
NativeRiskLimits risk;
risk.max_drawdown = NativeLossLimit{1000.0, false}; // account currency
risk.max_intraday_loss = NativeLossLimit{2.0, true}; // 2 % of the basis
risk.max_consecutive_loss_days = 3;
risk.max_fills_per_day = 10;
risk.day_basis = NativeRiskDay::SessionDay; // or CalendarDayInTimezone
risk.action = NativeRiskAction::BlockOpenings; // or FlattenAndBlock
spec.risk = risk;Where it is measured. At three points of every script bar: its open, its own close calculation, and once after each drain of applied fills. The mark is the marked equity at that point's own price — the bar's open, its close, or the fill's resolved price. A breach that first exists at the close is therefore acted on at that close: the openings of that same calculation are already refused, not the next bar's. A breach that exists only between the three points is seen at the next one.
| limit | breach | block lasts |
|---|---|---|
max_drawdown |
the marked equity has fallen from its running peak by at least the limit (percent: of that peak) | to the end of the run |
max_intraday_loss |
the marked equity is below the day's opening equity by at least the limit (percent: of that opening equity) | to the end of that day |
max_consecutive_loss_days |
N consecutive days have each closed with a realized loss | to the end of the run |
max_fills_per_day |
N applied fills within the day | to the end of that day |
A percent limit is out of 100, and is resolved against its own basis at the moment of the test. Limits are evaluated in the order above and one point reports at most one breach.
Counting fills. max_fills_per_day counts every applied execution of the
day as it settles, whatever issued it, and the limit is tested at the
evaluation points like every other. The fills matched at one point therefore
all settle — reaching the limit blocks the NEXT admission, not the fill that
reached it. That is the generic rule; TradingView's intraday order cap, whose
forced close also withdraws the working orders (its cancel-pending latch),
stays an adapter quirk on top of it.
Days. SessionDay keys on the run's own session calendar
(native_calendar::session_day_ordinal), so an overnight session is one day;
CalendarDayInTimezone keys on the plain civil date of the spec's scheduling
timezone. Every point of a script bar takes the day of that bar's own open, so
a bar never straddles two risk days. A day's realized result is the change in
realized balance across it: a day that closes negative extends the
consecutive-loss streak, one that closes positive restarts it, and one that
realized nothing leaves it alone. The streak is settled when the next day
opens.
The block. While it lasts, every opening is refused with
MatchRejectReason::RiskLimit, ahead of the per-opening caps. A reduction is
never an opening, so exits, brackets and the margin model's own liquidation
still fill. With NativeRiskAction::FlattenAndBlock the kernel additionally
issues one Flatten of its own at the breach point, carrying
RequestDefinition::origin == RequestOrigin::KernelRisk, and executes it
there; the block is latched before that flatten is submitted, so its fill
cannot re-enter the same breach.
The event. Every breach appends a NativeRiskEvent to the command
history, which native_events() returns like any other command event:
struct NativeRiskEvent {
uint64_t ordinal;
RiskLimitKind kind; // MaxDrawdown | MaxIntradayLoss
// MaxConsecutiveLossDays | MaxFillsPerDay
double limit; // the threshold in the unit it was measured in
double observed; // the value that reached it
std::int64_t day_ordinal;
MatchCursor cursor;
};NativeStrategyHost::native_risk_state() reads the ledger back at any time:
whether openings are blocked and why, the current risk day, the fills counted
in it, the consecutive-loss streak, the running peak equity and the day's
opening equity.
TradingView's strategy.risk.* is not this model, and stays in the Pine
adapter. The adapter never sets risk, for a structural
reason first: Pine's risk calls are per-bar script statements, so the adapter
learns a limit on the first script bar, after configure_native has fixed
and digested the spec — a begin-time declaration has nowhere to receive it.
Measured against the kernel on the same tapes, each rule also differs in
substance: the drawdown latch samples once per bar at the close and still
admits a reversal while latched (the kernel measures at the open, the close
and after each drain, and refuses every opening); the consecutive-loss streak
counts one losing trade per chart day and resets on any winning fill (the
kernel settles a day's net result at the next day's open); the intraday loss
is checked along the bar's path and closes at the adverse extreme, then
refuses every placement for the day and withdraws the working book (the
kernel checks at its three points, closes at that point under its own ticket
and blocks openings only); the filled-orders cap charges slots, transfers
quota, closes at the fill or the bar's better extreme and keys its day on the
chart timezone (the kernel counts settled fills, closes at the evaluation
point and keys on the spec timezone). Every difference is pinned, with the
adapter's rows harvested before the re-lowering, in
tests/test_adapter_risk_relower.cpp; the rulings and the corpus measurement
are in docs/design/native-feature-parity.md §3.6.
Native-only by ruling. That retention is final, and it is
not a gap: risk is a native-host feature whose consumers are native hosts,
in C++ and in C (PF_NATIVE_SPEC_EXT_RISK, strategy_native_risk_state_v1).
ADR-0001 ("Kernel capabilities the Pine adapter does not declare") records the
ruling and scripts/check_native_feature_rulings.py holds it. Two examples
exercise the block: native_trail_risk_strategy.cpp the fill-count cap with
BlockOpenings, and native_risk_limits_strategy.cpp the money limits with
FlattenAndBlock — a 1 % intraday loss limit that the kernel enforces with a
flatten of its own at the breach point (1200 observed against 1000, then 1000
against 988 of the next day's opening 98800), the re-arm at the next civil
day, and the consecutive-loss-days block that ends the run's openings.
NativeRunSpec::calculation decides when the kernel asks the host to
calculate. It is NativeCalculationTrigger::BarClose by default, which is the
established surface exactly: one calculation per script bar, at its close. The
source layer never sets this field, so Pine-compatible runs are unchanged, and
the spec folds the block into the continuation hash only once the trigger or
the open-bar view is non-default.
Every calculation — including the script bar's own — is delivered through
virtual void on_native_recalculate(const Bar& bar, const NativeDecisionContext& ctx,
NativeCalculationReason reason,
const native_order::ExecutionAppliedEvent* cause);whose default forwards to on_native_bar(bar, ctx). A host that implements
only on_native_bar therefore sees precisely what it saw before this field
existed. NativeCalculationReason is BarClose (the script bar's own
calculation, cause == nullptr), OrderFill (one recalculation at an applied
execution's cursor, cause being that event, valid only for the call),
Tick (one recalculation at a modeled point or an observed print) and
SubBar, which is reserved: a lower-timeframe sub-bar has its own hook and is
never delivered through on_native_recalculate.
The triggers are a strict superset chain, so opting in never removes a calculation:
BarClose(default): the script bar's calculation only.BarCloseAndFills: additionally oneOrderFillrecalculation per applied execution (Pine'scalc_on_order_fills, without its TradingView specifics).EveryModeledPoint: additionally oneTickrecalculation at every modeled point of the delivered path — each confirmed OHLC waypoint, each intrabar sample — and at every observed print, in batch and in a stream alike (Pine'scalc_on_every_tick, generically).
At one point, in this order and no other:
- Match and settle.
on_native_appliedfor each applied event, FIFO, through the existing notification drain under its re-entrancy guard.- With
BarCloseAndFillsorEveryModeledPoint, oneOrderFillrecalculation at that event's own cursor, right after itson_native_applied— and, for a kernel liquidation, after theon_native_margin_callthat follows it — driven from the same drain. It is bounded bymax_recalculations_per_point(default 8) per matching point: executions a callback drives throughexecute_currentland at the same cursor and spend the same budget, so a host that refills on its own fill cannot cascade without end. Beyond the bound the execution is still applied and still delivered toon_native_applied; only the calculation it would have driven is dropped.native_recalculation_count()andnative_recalculations_skipped()report both totals. - With
EveryModeledPoint, oneTickrecalculation after the point's matching is finished.on_native_tickstays the observation hook and still runs before the print is matched.
A request born in any of these callbacks follows the existing birth rule unchanged: it is eligible on the unconsumed rest of the bar, which for a market request means the next discrete matching point. Nothing about the drain order, the birth rule or language-state rollback moves — the kernel never attempts rollback; that stays a source-layer concern.
A recalculation records no report point. Under
NativeReportPolicy::KernelRecorded the equity curve still has exactly one
point per script bar whatever the trigger is.
virtual void on_native_sub_bar(const Bar& sub, const NativeDecisionContext& ctx);fires once after each retained lower-timeframe sub-bar's whole matching path,
before the next sub-bar's. It is not a cadence: it fires whatever
calculation is. It requires a real lower feed
(IntrabarPath::lower_tf) — a synthesized path and a plain confirmed bar have
no sub-bars of their own, so it never fires for them. The decision point is
the sub-bar's last modeled point, so commands and execute_current are legal
and a request born there follows the ordinary birth rule.
std::optional<Bar> current_partial_bar() const; // non-virtualanswers the lookahead-free bar so far: the open of this script bar's first
modeled point, the running high/low, and the close at the last path point the
walk has consumed. A discrete point — the open of the bar or of a sub-bar, a
sample of a DistributionSamples magnifier, an observed print of a stream — is
folded before its callbacks run, so there the bar so far closes at the cursor.
A fill inside a segment (a trigger crossed between two path points) happens
before the walk reaches the segment's destination, so that fill's
on_native_applied and its OrderFill recalculation read a bar that still
ends at the segment's origin: it never runs ahead of the cursor, and it does
not yet hold the fill price. volume accrues only activity actually consumed —
the completed lower-timeframe sub-bars of an intrabar path, or the observed
prints of a stream — and stays 0 for a modeled path that carries no intrabar
volume of its own. It is valid in the bar-open, applied, tick, sub-bar and
recalculation callbacks, and is nullopt outside a path walk, including in the
bar's own close calculation, where the host already holds the complete bar.
This matters because the mid-bar callbacks are handed the complete script
bar: on_native_bar_open receives the whole bar by default, and so does every
OrderFill / batch Tick recalculation. That is deliberate — a host that
schedules against the bar's own high/low needs it, and the adapter relies on
it — but it is lookahead. current_partial_bar() is the answer for a host
that must not see it.
NativeRunSpec::open_bar_view masks exactly one callback:
NativeOpenBarView::OpenOnly hands on_native_bar_open (and current_bar_
while it runs) H = L = C = open and volume 0. Complete (default) is
unchanged. The mask is presentation only: the complete bar is restored before
the open match, so matching, fills, excursions and every later callback are
byte-for-byte what Complete books.
The Pine adapter no longer schedules its own fill cascade: a
calc_on_order_fills strategy projects calculation = BarCloseAndFills with
max_recalculations_per_point set to TradingView's guard literal, and the
consumer drives every fill recalculation from its notification drain and
delivers it as on_native_recalculate(..., OrderFill, cause). What remains in
src/source is only what TradingView's COOF adds on top of that cadence
: the language-state snapshot/restore/commit around a
recalculation, the waypoint-only refill deferral, the two-fills-at-open rule,
the first-open execution chain, and the two fills Pine refuses to recalculate
on at all — a process_orders_on_close fill at the already-consumed close, and
a grouped same-entry stop sibling. Those refusals compose with the kernel
cadence rather than replacing it: the recalculation is offered at the cursor
and the source layer publishes nothing. calc_on_order_fills is a batch-only
pairing; the Pine stream route refuses it before any spec is projected, so
BarCloseAndFills never reaches forward execution. The waypoint deferral is
still load-bearing: a native BarCloseAndFills host running the adapter's
refill rule reaches the same book with the same order ids in the same order,
but the coordinates a request born in a recalculation fills at differ — the
adapter re-presents it at the chart bar's next waypoint, the kernel at the next
discrete matching point of the delivered path.
The repository holds one fill simulation: src/native_matching.hpp driven
by NativeExecutionConsumer. The TradingView exit-path resolver that used to
sit beside it — internal::resolve_exit_path_fill, a 930-line intrabar walk
in src/source/pine_path_resolve.cpp — has no declaration, no shipped body
and no test-only copy left; the re-lowerings took its last production caller
and the header that preserved its body for the trail suites was retired with
it. src/source/pine_path_resolve.cpp still holds
internal::entry_stop_first_touch (pine_path_resolve.cpp:36), the one
function of that file a live caller reaches.
Those rows are now a matcher-side projection. tests/trail_exit_product_probe.hpp
builds each resolver scenario — a position at an entry price with a carried
running extreme, one strategy.exit trail, one probe bar — as three script
bars through the real host: the adapter lowers the trail the way it lowers
every corpus strategy, the consumer walks the bar's synthesized path, and the
fill is read back from NativeStrategyHost::native_events — the applied
event's modeled price and match cursor, the terms event's price kind, the
closing request's trigger kind — beside the booked trade row. The four fields
the resolver reported are projected from that record: at_bar_open is the
cursor's NativePathPhase::Open, path_position is the cursor's waypoint
index and t in the legacy first_touch_position units, is_trail is the
kernel trigger the adapter chose (a generic Trail, or the one-shot Limit
spelling of a zero-tick offset), and a level fill is
NativeCandidatePriceKind::TriggerLevel. Where the two disagree on a
coordinate rather than on a price — a one-shot's crossing is the adapter's
half-tick arm threshold, half a tick before the activation, not the level —
the row carries its expectation corrected: note, and the three suites are
registered in tests/twin_parity_inventory.json's observableRewrites with
their new assertion digests. Every TradingView number those suites pin is
unchanged.
P9 left one ruling open, and lane E5 settled it with TradingView's own tapes
(the private fixture set offset_trail_arm, replayed by
tests/test_offset_trail_quantized_arm.cpp): a trail WITH a trailing offset
arms on the tick-quantized path exactly like the one-shot. On NYSE:F 15m, six
trades whose bar's raw extreme stops inside the activation's tick cell
(9.415, 13.041, 12.641 lows; 11.899, 13.049, 13.419 highs) exit on that bar at
activation -/+ the offset; on the on-grid ETH feed a sub-tick activation is
never rounded onto the fill. The adapter therefore arms the generic Trail
at the half-tick threshold its one-shot leg rests at and books from a running
best that starts at the activation; the kernel still compares arm_price
with the raw path. The P9 row that asserted the hold in that window now
asserts the fill, with its expectation corrected: note.
Lane E9 closed the two findings E5 left
(the private fixture set trail_activation_tick_reach, replayed by
the private test test_trail_activation_tick_reach). A one-shot with a sub-tick
trail_price books the tick the quantized path reaches — its resting half-tick
level rounded away from the position (ETH fill 2550.85, level 2550.854,
@2550.86) — where the adapter had booked the snap toward it, which the kernel's
limit-or-better check rightly refused (InvalidTerms). And a placement close
inside the activation's tick cell (NYSE:F 11.295 under 11.30) has reached the
activation: 8 of 8 trades exit on the next bar, trail_offset 1 and 0 alike,
so P9's carried-best row (9.996 under 10.00) now asserts the fill @9.98, with
its expectation corrected: note. Both stay adapter policy; the kernel is
unchanged.
Lane E14 closed the residual both of them left. TradingView's running best
starts AT the activation; the kernel's started at the arm — half a tick short
of it on a crossing, at the next bar's first print when the placement close
armed it — so a print between the two fires TradingView's stop and not the
kernel's. None of the fourteen tapes fell in that window, but a shallower
next bar would. The generic answer is Trail::best_seed above: the leg names
the level its ride starts at, and the adapter names the activation, or the
favourable one of the activation and the placement print when that print
already armed it. The kernel's own crossing is then the price the adapter
books, which is what the three trail twin suites now assert. The sibling stop
an explicit-zero trail rests beside its Trail stays: a Stop is reached by
a touch and a zero-distance ride needs a move strictly past the best, so they
differ exactly on a print that lands ON the carried best. TradingView books
the touch at the open: 16 of 16 taped trades of that shape, which a kernel
host reproduces only with both legs (the private test test_zero_trail_sibling_stop).
Two kernel helpers went with it. BacktestEngine::round_to_mintick_directional
and BacktestEngine::apply_slippage were the pre-R5 spelling of the
directional tick snap and the slippage step; the kernel fills on
native_matching::grid_round_directional and native_matching::apply_slippage
instead (native_execution_consumer.cpp), so the pair had no caller in src/
at all and has been deleted from the public header. A native host that wants a
computed level on the ladder declares NativeRunSpec::price_grid; it does not
snap by hand.
There is one engine lot/account book. Native matching inspects settlement, admits openings against the frozen spec, then commits once. Fees are quoted on that execution (percent of absolute notional, cash per unit, or one cash ticket per execution). Slippage is applied to the raw observed/modeled price as described above. Native account rows follow lots, remaining entry costs, and realized balance at the matching coordinate.
Every query below is a const member of NativeStrategyHost that copies
owning values out: a row already returned is never invalidated by a later
command or by the reset of the next run. Each has a C spelling unless the row
says otherwise, and the C spellings are in
<pineforge/native_c_api.h> (see Driving the kernel from C). A C spelling
marked partial carries a subset of the C++ record; the 1.0 C boundary table
there lists what it leaves out.
| query | answers | C spelling |
|---|---|---|
native_state() |
NativeStateView native_host.hpp:282: the lifecycle kind, the staged spec, the phase, the completion, the durable failure, the consumed high water and the decision floor |
strategy_native_state_v1 (partial: no staged spec, no failure context) |
physical_position() |
NativePhysicalPosition native_host.hpp:296: signed_units, average_price, lot_count |
strategy_native_position_v1 |
native_open_lots(mark) |
one NativeOpenLot native_host.hpp:328 per open physical lot, oldest first, marked at mark |
strategy_native_open_lot_count_v1 / _get_v1 |
native_marked_equity(mark) |
the account's marked equity at mark |
strategy_native_marked_equity_v1 |
native_working_requests() |
one NativeWorkingRequest native_host.hpp:681 per live request: its definition, its remaining and its trigger_state |
strategy_native_working_len_v1 / _get_v1 (partial: part of the definition) |
trail_state(handle) |
NativeTrailState native_host.hpp:669: activated, best_price, current_level, activation_ordinal; nullopt when the handle is not a live trail |
strategy_native_trail_state_v1 |
native_events(after) |
NativeMarketEvent native_host.hpp:370 rows: a NativeEventKind native_host.hpp:360 (Command, Driver, Account) and exactly one of command, driver (NativeDriverPoint market_driver.hpp:97) or account (NativeAccountObservation native_host.hpp:351) — the rows the run's event_retention keeps (What a run keeps of its events) |
strategy_native_events_v1 (partial: each kind's documented fields) |
native_event_window_start() |
the oldest ordinal a read can still return: every command event at or above it is retained; 1 while nothing was dropped | strategy_native_event_window_v1 |
current_execution_point() |
NativeCurrentPointView native_host.hpp:658: the active callback's decision context, its price, the NativeCurrentQuoteKind native_host.hpp:451 and the ordinal the quote came from; nullopt outside a decision point |
pf_native_decision_v1::price / ::quote_kind (partial: no quote origin) |
native_risk_state() |
NativeRiskState native_host.hpp:643: whether openings are blocked and why, the risk day, the fills counted in it, the loss-day streak, the peak equity and the day's opening equity |
strategy_native_risk_state_v1 |
native_liquidation_price() |
the solved level L, or nullopt |
strategy_native_liquidation_price_v1 |
native_aggregates_input_bars() |
whether the run's script bars are buckets the kernel gathers from its input bars: native_calendar::pairing_aggregates of the pairing it resolved for the spec's two timeframes; false on a chart-timeframe run |
none — a C host names both timeframes in its own spec |
current_partial_bar() |
the lookahead-free bar so far, through the last path point consumed | strategy_native_partial_bar_v1 |
native_series_bar(i) |
the latest delivered bucket of subscription i |
strategy_native_series_bar_v1 |
native_recalculation_count() / native_recalculations_skipped() |
the calculations the cadence drove and the ones its per-point bound dropped | strategy_native_recalculations_v1 |
native_decision_floor() (native_host.hpp:1361) |
the run's monotonic decision floor in epoch ms — the same value NativeStateView::decision_floor_ms carries, and the lower bound every request's birth is compared against |
pf_native_state_v1::decision_floor_ms |
native_consumed_high_water() (native_host.hpp:1366) |
the highest run_number this host has consumed. It lives outside per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 |
pf_native_state_v1::consumed_high_water |
native_continuation_hash() |
the consumer's continuation identity: a fold of its live state (What the continuation and the broker-state hash fold), the timezone folded by its content, so the same spec over the same bars and zone rules answers the same value on every host | strategy_native_continuation_hash_v1 |
native_sized_units(sized, price, equity, fx) |
the kernel's own Sized resolution as a pure query |
strategy_native_sized_units_v1 |
inspect_current_execution(cmd) |
NativeCurrentExecutionPreview, with a NativeCurrentRefusal native_host.hpp:697 when the command cannot be consumed here |
none in 1.0 (scheduled for 1.1.0): strategy_native_execute_current_v1 applies the command and is no preview |
Reading a failure. native_state().failure is a NativeFailure
(native_host.hpp:237): a NativeFailureCode, the NativeFailureOperation
it happened in, an optional ordinal and discriminator, and an
allocation-free context. The context is a tagged union chosen by
NativeFailureContextKind (native_host.hpp:97), and which of its three
in-run facts is populated is answered by three constexpr predicates rather
than by reading the tag yourself:
native_failure_has_cause (native_host.hpp:145),
native_failure_has_recipient (native_host.hpp:149) and
native_failure_has_cursor (native_host.hpp:153) — each taking either the
kind or the whole context. native_failure_context_in_run
(native_host.hpp:207) builds one; native_failed_run_identity reads back
the RunIdentity the failed spec carried, and a foreign run is dropped
rather than relabelled. Failure copy and move do not allocate.
last_error() is presentation text beside it, never the authority.
Callback contexts. The bar callbacks receive
NativeDecisionContext (market_driver.hpp:120): the point's
NativeCoordinate (market_driver.hpp:72 — ordinal, the script-bar
interval_index, the named input-slot input_interval_index, the interval
stamps, the effective time, the NativePathPhase and the
NativeCompletionKind),
the decision floor, both calendar intervals, and the script bar's
session-day facts (R5 lane F5): in_session (market_driver.hpp:161),
opens_session_day, closes_session_day and
closes_session_day_open_ended. They read the run's own calendar and
session day — the cycle that rolls at the session's first window start,
keyed to its trading date, so an overnight session is one day across local
midnight — each bar at its own label, or at its interval's first eligible
instant when that is later (a bar opening inside a declared break), and the
bar before or after the script bar: the one the run
holds (its batch input or stream warmup), otherwise the calendar's previous
or next eligible input slot, across declared breaks. A run's first bar opens
its session day and a batch's
final bar closes it; the open-ended close reads the calendar at that final
bar instead, for a host whose last input is still forming; a D/W/M bar holds
whole days, so all four are true. Every callback of one script bar carries
the same four, fills and fill recalculations included, and a C host reads
the first three from pf_native_decision_v1's session bytes
(native_c_api.h:1485). The kernel resolves each session day once through
native_calendar::session_day_at (native_calendar.hpp:455), which a host
may call too. the private test test_native_session_day_facts replays the
TradingView session tapes through a bare host. It is a presentation snapshot
copied onto the callback stack: writing to it cannot move the floor, the
matching time or the after-calculation coordinate, and the session-day facts
fold into no digest. on_native_input receives
NativeInputContext (native_host.hpp:773) instead — the two intervals, the
input index and whether this input completes the script interval — and
on_native_tick receives NativeTickContext (native_host.hpp:785), the
decision context plus the print's sequence, where zero keeps the public
TradeTick sentinel meaning "the provider supplied none".
Cohorts. A cohort is a host-built roster of openings a later close binds
to: cohort_open() answers a native_order::CohortHandle, cohort_add
(native_host.hpp:1285) enrolls one accepted opening's handle,
cohort_remove (native_host.hpp:1288) takes it back off, and a request with
owner = native_order::BindCohort{cohort} closes what the roster holds at
the match. The C spellings are strategy_native_cohort_open_v1 / _add_v1 /
_remove_v1; in C the pairing is fixed — a BindCohort owner is reachable
only with PF_NATIVE_INTENT_HOST_SIZED, whose units the on_close_units
hook answers. tests/test_native_adapter_lowering_l1.cpp pins enrollment and
the cohort close from a bare host, and the cohort scenario of
tests/test_native_c_api.c does the same from C.
NativeRunSpec::event_retention (NativeEventRetention, R5 lane V19-B)
chooses what native_events() can still return. It is reporting: no fill, no
decision and no value the continuation folds depends on it, except that the
spec digest folds a retention other than the default.
| retention | what the kernel keeps | memory |
|---|---|---|
Window (the default) |
the command journal, only until the host has read it; no driver point, no account row | O(live + replaces) |
Commands |
the whole command journal and every account row; no driver point | O(run) |
Full |
the whole command journal, every driver point and every account row — the record every run kept before the field existed | O(run) |
Under Window a host that polls says what it has read:
native_acknowledge_events(through_ordinal) records that every event through
that ordinal is consumed, and the kernel drops the acknowledged command events
at the next script-bar boundary, keeping every one above it. Until a host
acknowledges, the kernel treats it as served by its callbacks alone and closes
its window at every script-bar end, so a polling host acknowledges 0 from
on_native_run_begin ("nothing read yet") and then its cursor as it reads.
An acknowledgement is never lowered by a later, smaller one; one above the
event high water acknowledges the high water, never an event that has not
happened; and before a run begins, or once it has ended, it records nothing.
Full and Commands keep the whole journal whatever is acknowledged.
native_event_window_start() answers the oldest ordinal a read can still
return, and native_events(after) for an after below it starts there.
The kernel never drops an event its own live state still reads. A queued
applied notification (and the margin receipt it carries) pins the window at
its execution, a live deferred group-adjustment chain pins it at its head,
and the rest of what the journal used to answer is state now: a request's
replace-chain root is RequestDefinition::root (native_order.hpp:736) and
the order core's chain index, which is all a cohort command reads about a
request that is no longer working; a trail's arm ordinal
(NativeTrailState::activation_ordinal) is its tracking state's
(TrailTrack native_order.hpp:650); the FX-roll margin check reads the
last two driver points' instants; and a group-effect receipt whose outcome
event was dropped stands on its own record. A host that wants the whole run
after it has ended asks for Full (or Commands); the Pine adapter declares
Window and acknowledges its receipt cursor as it observes the journal.
A C host spells the retention in pf_native_run_spec_ext_v1's fifth layout
(below). A C caller that does not send it — strategy_configure_native_v1,
any earlier layout, a clear bit — keeps Full, the record its layout was
published with.
tests/test_native_event_retention.cpp (kernel-only) holds the readbacks to a
Full record pinned before the window existed, row for row, over randomized
books; pins every acknowledgement edge above; and runs the same books with the
window closing at every driver point (a test switch), booking the trades
Full books. tests/test_native_journal_window.cpp holds the chain index to
a journal oracle and a live deferred chain to its pin.
fill_report publishes closed trades, diagnostics, an equity curve and the
metrics derived from it. The curve is host-owned by default: nothing in
the kernel records a point, so a bare NativeStrategyHost that leaves
report_policy at HostRecorded reports equity_curve_len == 0, and every
metric derived from that curve (pf_equity_stats_t: its drawdown, run-up,
Sharpe/Sortino, CAGR, time in market) degenerates over the empty series. A
host that marks its own equity keeps this default and owns the whole series.
The scalar extremes are not part of that bargain. max_drawdown_,
max_runup_ and max_contracts_held_all_ / _long_ / _short_ — read back
through max_drawdown_percent() (engine.hpp:1507), max_runup_percent()
(engine.hpp:972) and max_contracts_held_all/long/short()
(max_contracts_held_all engine.hpp:1852-1854) — are a property of the RUN: what it drew down, what
it ran up, the most it ever held. The kernel folds them (update_equity_extremes, engine.hpp:1362) at every script
calculation under HostRecorded and KernelRecorded. Under
KernelRecordedAtHostMarks, it folds them at the host-mark callbacks instead, so
the host controls the cadence. A HostRecorded host reads them truthfully without
asking the kernel to record a curve. Before R5 lane E2 the fold was
reachable only through the two recording policies, and a defaulted host read
all five back as zero for the whole run. What stays policy-scoped is the
recording: the curve point, and with it every pf_equity_stats_t figure.
NativeReportPolicy::KernelRecorded asks the consumer to record instead. Once
per script calculation — after the callback returns, and after the
AfterCalculation close when that mode is on — it appends one point labelled
with the script interval's open, at the same instant as that
calculation's extremes fold, so the curve is identical with and without an
intrabar path and a re-walk of it reproduces the scalars above bit for bit
(compute_equity_stats, engine_metrics.cpp:203, "MUST mirror
update_equity_extremes"). The result is one point per script bar, a finite
drawdown/run-up walk, and metrics computed over a real series.
The per-bar broker-state hash is a row of that same report, so
KernelRecorded records it too. It stays behind the recording switch it
always had — set_broker_state_hash_recording(true)
(engine.hpp:2143; C: strategy_set_broker_state_hash_recording), off by
default, set while no run is active — because each row is a full
broker_state_hash() over the lots and the closed rows (since v19 a row costs
the live state, not the run's length: the closed rows enter through a running
digest). With the switch on,
one row follows each point, after the extremes that point just folded
(record_script_report_point, native_execution_consumer.cpp:8768), so
broker_state_hash_len == equity_curve_len == script_bars_processed
on a completed run in batch and across a stream's warmup and realtime legs alike. A Failed or Aborted run may expose only a recorded prefix, whose last row need not equal the final scalar. That length identity is the only part of the array that holds across drivings.
What a row is, and what it is for. A row is the run's continuation
identity at that bar, not its trade outcome. broker_state_hash() is
broker_state_hash_from_execution_hash(continuation_hash()), so a row folds
the kernel's broker state — the lots, the realized sums, the equity extremes,
the closed rows — and, ahead of it, the state a resume would continue from.
NativeRunPhase (Batch / Warmup / Realtime, readable as
native_state().phase) is folded into that continuation on purpose: a
consumer mid-warmup and a consumer mid-realtime are not interchangeable
continuations, which is exactly what native_continuation_hash() is for and
why stream_state_hash() leads with this value. So the array is a replay
check within one driving mode and deliberately not across modes:
- Same driving. Two runs driven the same way over the same bars record the same rows, and the row after bar k equals the last row of a run driven the same way that ended at bar k. That prefix closure — in a batch, and in a stream at any warmup split — is what makes the array a replay check.
- Different driving. The same bars booking the same trades record
different rows under
run(), understream_begin(warmup=1)+stream_push_barand understream_begin(warmup=all), differing from index 0. Two streams do not promise identical warmup rows once subscriptions are declared: each subscription carries its own delivery cursor and warmup boundary. Compare outcomes through the native twin; continuation rows remain a within-driving replay check. - The broker half alone is driving-mode invariant. Factor the continuation
out —
broker_state_hash_from_execution_hash(fixed), protected onBacktestEngine— and the remaining fold is identical at every bar in every driving. The divergence is the continuation and nothing else.
The batch↔stream oracle is the outcome, not the hash. What pins that a
stream books what a batch books is the outcome twin: section 8 of
tests/test_native_margin_fx_roll.cpp compares every offered check point,
every measured requirement, every receipt, every closed row and the book that
is left; tests/test_streaming.cpp compares trade count, position, equity and
each closed row's identity, prices and size. For the Pine adapter,
scripts/check_corpus_parity.sh holds every validation-corpus probe's batch
trades to a pinned baseline; it drives no stream.
Both directions of the row contract are pinned in
tests/test_native_report_truth.cpp.
Recording is reporting — the switch is not continuation state and no row is
ever read back, so a run records the same trades, curve, continuation and
broker state with it on or off. Under HostRecorded the report is the host's
and the kernel appends nothing; a C++ host that records its own report
appends its own rows.
NativeReportPolicy::KernelRecordedAtHostMarks records the very same series
at the points the host marks, for a host whose report cadence is not one point
per calculation. The consumer never records on its own initiative under it:
the host calls mark_native_report_point(report_ts) from inside its own
callback, naming the label the point carries. The kernel performs the extremes
fold and curve append. The call returns true when it appends; it returns
false and changes nothing outside a running KernelRecordedAtHostMarks run.
A C host has no call that marks one (the 1.0 C boundary table).
The Pine adapter is one marking host — its report has one point per
published source slot, which is not the same series of instants: a
calc_on_order_fills re-entry marks the slot it opened at the fill and the
bar's ordinary close calculation then marks nothing, and a suppressed probe
tail advances source history, and marks, without running generated code at
all. The point also has to land inside the callback, before the adapter takes
that bar's broker-state hash and before its range-end rows re-mark the curve's
last point. Under this policy the kernel owns what a report point is and the
host owns only when. The marking host also appends its own broker-state hash
row — the adapter's row has to follow a continuation snapshot only it can
name — so the kernel appends none under this policy.
report_open_position_at_end (KernelRecorded only) adds the rows a close of
the still-open position at the last bar's close would record — one per
physical lot, through the same row builder every full close uses, with
open_at_end set. The mark is the raw close on the price grid with no
slippage: slippage models a market order's fill uncertainty, and this row is
a mark, not an order. It is reporting only. The live position, the pending
orders, the realized sums, the equity curve and the broker state are left
exactly as the run left them; the rows appear in fill_report and in
report_trade_count() / get_report_trade(), never in closed_trade_count()
/ closed_trade().
The rows come from one generic producer inside the consumer, which appends a
mark-to-market row per open lot and answers the summed net row P&L. That
producer is policy-free: it does not choose when to mark, what to clear first,
or whether anything downstream of the rows is re-derived from them. This is
what lets a host whose report has a different shape — TradingView's range-end
report re-marks the equity curve's last point off the net row P&L, re-folds
every extreme from it and does so at three marks on the terminal bar — drive
the same producer at its own marks instead of restating the loop, while
KernelRecorded keeps the plain run-end behaviour described above. The two
shapes are measured against each other in
docs/design/native-feature-parity.md §3.7.
Both fields are opt-in and fold into the continuation hash only under
KernelRecorded — the one policy under which the consumer decides, on its
own, to act between two points of a run. HostRecorded and
KernelRecordedAtHostMarks fold nothing, so a spec that does not hand the
kernel its cadence keeps the continuation identity it had before these fields
existed. Recording does move the broker-state hash, because the equity
extremes it folds are durable engine state.
Per-trade reads: closed_trade_count() / closed_trade(i) return the closed
rows this run booked; report_trade_count() / get_report_trade(i) span those
rows followed by the range-end rows, in the order fill_report lays them out.
closed_trade(i) is the row itself, by reference — the object get_trade(i)
and get_report_trade(i) read at the same index and the row fill_report
publishes there, field for field; a range-end row counts in the report space
only. Pinned by closed_rows_by_index in
tests/test_native_report_truth.cpp.
Every closed row carries the cause its closer recorded:
execution::CloseCause (execution.hpp:34) — Unspecified, Script,
Bracket, Liquidation, RiskLimit, FillCap, RangeEnd. A
kernel-originated close states it through the settling fill, which is how a
bare host's own rows get theirs: the margin model's liquidation books
Liquidation and a FlattenAndBlock risk breach books RiskLimit, both
under the ticket the model or the run named. A host running its own forced
close states the cause on the row it produced.
closed_trade_close_cause(i) (engine.hpp:1849) is the C++ read and
strategy_closed_trade_close_cause (pineforge.h:1309) the C one, with the
same numbering: -1 for a bad index or a NULL handle, 0 UNKNOWN, 1
SCRIPT, 2 BRACKET, 3 MARGIN_CALL, 4 INTRADAY_LOSS_CAP, 5
INTRADAY_FILL_CAP, 6 RANGE_END. A row closed at the end of the run
(open_at_end) always answers 6, ahead of every other cause. The ticket a
row was booked under is strategy_closed_trade_entry_id /
_exit_id / _exit_comment (pineforge.h:1242-1257), which index exactly the
rows of fill_report's trade array and take any handle this engine produces
— including a pf_strategy_t from strategy_native_host_create_v1, which is
how a C host reads back the ticket its own margin model declared.
broker_state_hash() folds the kernel's own broker state: the execution
continuation, the position and its lots, the realized sums, the equity
extremes, the closed rows. A host whose next decision also depends on state
the kernel does not own — a regime, a counter, a model — folds that state in
through one generic hook, so a replay that diverges there diverges in the
hash:
struct RegimeHost : pineforge::NativeStrategyHost {
std::int64_t regime = 0;
void hash_host_extension(pineforge::BrokerStateHashSink& sink) const override {
sink.s("regime-host/v1"); // your own domain tag first
sink.i(regime); // then each durable value, in a fixed order
}
// on_native_bar(...) reads and moves `regime`
};hash_host_extension(engine.hpp:405, protected virtual onBacktestEngine) is called exactly once per hash, last, after the kernel's fold. What it writes is part of the scalarbroker_state_hash(), of every per-bar row aKernelRecordedrun records, and ofstream_state_hash().BrokerStateHashSink(engine.hpp:350) is a complete public type: FNV-1a over a canonical byte spelling —d(a double;-0.0folds as0.0, every NaN as one quiet NaN),i,u,b,s(length, then bytes),bytes.- An override replaces the default. A host that overrides nothing folds
the marker
"source:none", exactly the bytes it folded before the hook existed; callBacktestEngine::hash_host_extension(sink)first to keep the marker and append to it. - The extension is input to the hash only. It moves no fill and is not part
of
native_continuation_hash(): two hosts that trade identically keep identical trades and continuations however their own folded state differs. hash_source_extensionis the deprecated spelling of the same seam, from before a bare host could extend the fold. The generic default forwards to it, so a subclass still written against it compiles and hashes unchanged; new hosts overridehash_host_extension. The Pine adapter folds its own state through the generic hook like any other host.- A C host folds its own durable state through
pf_native_callbacks_v1::on_hash_extension, the C route ofhash_host_extension: it answers a 64-bit digest of that state, which the kernel folds after its own bytes with a domain tag, in every per-bar row, the final and the stream hash. Without it the C host's broker-state hash is the kernel's own fold. The per-bar rows need no hook and are available to a C host as described above.
Both digests fold state, not history (the v19 value epoch, R5 lane V19-A:
native-consumer/v9, pineforge-broker-state/v19, stream fingerprint 19).
native_continuation_hash()folds what a resume continues from: the three semantic versions; the lifecycle and its phase; the bound session, the decision floor and the callback context; the current execution frame and the queued notifications; the driver's cursors, its script and forming bars and its statistics; the declared series' cursors and an auxiliary feed's appended rows (their count and digest); the applied spec as one digest, taken once per spec and run generation, and the calculation-timing, margin and risk state a spec opts into; the live request table and the cohort rosters; and the order core's two counters, its last committed ordinal and incarnation. What the core carries forward beyond its live tables folds as three running digests, each record folded once when it commits: the group-effect receipts, the cohort receipts, and a compact record of every committed event — its kind and reason — which keeps visible a difference that leaves no other trace, such as a Cancelled receipt's reason (the host's own cancel against an owner's). The command history, the driver log and the account log are not folded: they are readbacks of how the run got there. What the journal window made state (R5 lane V19-B) folds where it exists: the order core's chain index — each replace successor's root, once, at the replace — a trail's arm ordinal in its tracking state, and, under a staged FX curve and a margin model, the two driver-point instants the FX-roll check reads. The acknowledgement and the event high waters are readback bookkeeping and fold nowhere. So equal state answers an equal value, two command histories that reach one state answer one value, and a read costs the live state whatever the run's length. A cohort roster is live state as its host keeps it: every member folds at every read, so an origin the host leaves on its roster after nothing can bind it any more — its request chain no longer works and no lot it opened is still open — costs every later read. The Pine adapter takes each such origin off its roster (cohort_remove) at the next bar open (R5 lane V19-FIX), so a Pine run's rosters hold the openings that still work or hold a lot, however many the run has made. The fold takes one multiply-xorshift per 64-bit word; a string folds as its length, then its bytes eight at a time.native_run_spec_digest(spec)keeps its byte-wise FNV-1a and its values.broker_state_hash()folds that continuation, the position and its lots, the realized sums and the equity extremes as before, and the closed rows as their count and a running digest of each row's entry and exit time, entry and exit price, quantity and P&L, folded once per row. A row is final once the applied notification of the execution that booked it has returned; the host may amend it inside that notification, and a read before then folds the row without freezing it. A host that changes a final row — the Pine adapter reorders a bar's trailing same-bar bracket exits after their notifications — names the first row it changed throughNativeStrategyHost::native_closed_rows_amended(first_row)before the next read, and the rows fold again from there. Rows removed from the end need no name: the kernel only appends, so a read over fewer rows, or a booking below the final mark, forgets them. Debug builds re-fold every digested row at each run's end and abort on a final row changed without a name. A C host never writes a closed row and has nothing to name.stream_state_hash()leads with fingerprint version 19 and the broker-state hash above.- The continuation a run's final
broker_state_hash()folds is latched at the last script point and taken at once (R5 lane PERF-P1 had latched a view folded on first read; with no history in the fold there is nothing a view saves).
The witness is tests/test_native_state_continuation.cpp (kernel-only):
every folded member moves the value and restoring it restores the value; two
histories, one state, one value; the Cancelled reason still differs; per-bar
recorded broker hashes cost linear time; and the closed-row finality rule.
Timestamps are Unix milliseconds. Confirmed bars require positive finite
OHLC (high ≥ max(open, close), low ≤ min(open, close)) and finite nonnegative volume,
strictly increasing non-overlapping input slots, and a canonical slot label
(interval.open_ms or the scheduled clipped eligible_open_ms). Supply
positive prices: ticks require a positive finite price, and matching
rejects a nonpositive resolved price.
Timeframe literals (spec is the authority; duration is never inferred from the first two bars):
- seconds
nS("15S") - minutes
"1","5","60","240"(noHsuffix;"1H"is not a minute) - days
"D"/"nD", weeks"W"/"nW", months"M"/"nM"
Count ≥ 1, no leading zeros, no whitespace. "D" is not rewritten to "1D".
See native_calendar.hpp for parse/pairing enumerators.
Supported pairings: equal-literal passthrough; same unit with script count a
multiple of input; fixed second/minute durations where script ms is a
multiple of input ms; fixed → D/W/M; D/nD → W/M and W/nW → M. Script finer
than input, or indivisible fixed ("5" → "7"), is refused at setup.
Batch admits monthly input (M / nM) on those pairings. Stream
refuses monthly input before begin (and the runner refuses it before
binding a ledger). Stream D/W uses civil/session next-open stepping.
Each confirmed input is attributed wholly to the script period that contains its input open (OHLC/volume are not split across a calendar boundary). Script calculation time is not earlier than the latest included contributor close. A trailing coarser script bucket is calculated at batch end when its final contributor reaches the calendar's last-traded close. Input that stops inside the interval leaves its bucket unfinished.
Session strings: empty or "24x7"; comma-separated HHMM-HHMM windows;
2400 as a legal end; overnight wrap; equal start/end as a full day from
that clock; optional :1234567 mask (1 = Sunday … 7 = Saturday). The
first declared window start is the civil-cycle origin. Timezone names,
offsets, and POSIX forms are accepted or refused by
native_calendar::timezone_accepted; see that header rather than guessing
IANA aliases.
Batch may be sparse (missing in-session slots are allowed; no synthetic
prices). Stream warmup requires at least one input and every provided
input slot to be a complete confirmed interval. Under Canonical, missing
in-session bars are refused; under FeedTolerant, warmup may omit them,
including an entire trading day. Realtime stream_push_bar still refuses an
in-session gap under either policy. A calendar day mask must exclude days a
stream is expected to skip, such as weekends. An unfinished coarser script
bucket is retained through
Warmup → Realtime. The handoff does not invent a script callback, a future
exclusive close, or a raised floor to a future seal. Empty batch is a valid
completed run with no events.
A bare NativeStrategyHost reads a coarser series of its own symbol — Pine's
request.security(syminfo.tickerid, tf, …) — by declaring it in the run spec.
The kernel aggregates the accepted input into the declared buckets; there is
no source layer and no Pine expression.
NativeTimeframeSubscription hourly;
hourly.tf = "60"; // pairs with input_tf like script_tf does
hourly.lookahead = false; // barmerge.lookahead_off (the default)
hourly.gaps = false; // barmerge.gaps_off (the default)
// hourly.authoritative_bars = …; // optional exchange bars, see below
spec.subscriptions.push_back(hourly);
class Host : public NativeStrategyHost {
void on_native_timeframe_bar(const Bar& bucket,
const NativeTimeframeBarContext& context) override {
// context.subscription indexes spec.subscriptions
}
void on_native_bar(const Bar&, const NativeDecisionContext&) override {
if (auto hour = native_series_bar(0)) { /* latest delivered bucket */ }
}
};subscriptions is empty by default and every part of this section is inert
for a spec that declares none: no evaluator is registered, no feed is
prepared, on_native_timeframe_bar is never called, and the run's
continuation hash is the pre-subscription one.
A subscription is a series instance, not a period. Several may declare the
same tf; each gets its own evaluator and bucket state, and each is
identified by its own index — in NativeTimeframeBarContext::subscription and
in native_series_bar(index). Two "60" series over a 15-minute input
deliver the same buckets twice, once per index, in declaration order. The one
thing same-period instances cannot each own is a different
authoritative_bars feed: the feed store is keyed by the period's duration,
so declaring two conflicting feeds for one period is refused
(DuplicateSubscriptionTimeframe), while declaring the same bars twice — or
leaving one, or both, empty — is accepted.
Validation (at configure_native). Each tf must parse and must pair
with input_tf exactly as script_tf does. Named refusals:
NativeRunSpecError |
Cause |
|---|---|
InvalidSubscriptionTimeframe |
unparseable literal, or a pairing script_tf would not accept |
SubscriptionFinerThanInput |
strictly finer than input_tf — a lower-timeframe array is a different contract and is not promoted |
DuplicateSubscriptionTimeframe |
two series of the same period declaring different authoritative_bars (the feed store is keyed by duration, and every monthly literal is one period, so they cannot each own one) |
UnorderedSubscriptionBars |
authoritative_bars not strictly increasing in time |
SubscriptionWithoutTimeframe |
declared together with timeframe_undetected |
SubscriptionWithoutAuxiliaryFeed |
a series whose source is AuxiliaryFeed in a spec that declares no feed |
SubscriptionFinerThanAuxiliaryFeed |
a series built from the feed and strictly finer than it |
UnknownSeriesSource |
a source outside its enumeration |
Delivery. A bucket is delivered on an accepted input bar, before that
input is aggregated, matched or calculated — so on_native_input precedes it
and the input's on_native_bar follows it.
lookahead = false(Pine'sbarmerge.lookahead_off): the bucket is delivered on the input bar that completes it, never earlier — its last contributing input bar when that bar closes it (Confirmed, a bucket the session close or a break in the session clips short of its nominal end included: the calendar knows that bar is its window's last), or, for a bucket only a later input reveals as complete (a hole over its last slot), the next period's first input bar (LazyComplete), which contributes nothing to it.lookahead = true(barmerge.lookahead_on): the completed bucket's final OHLCV is delivered at its first contributing input bar, andnative_series_baranswers with it from then on.gaps = false(Pine'sbarmerge.gaps_off, the default): a delivered bucket stands until the next delivery replaces it.gaps = true(barmerge.gaps_on): the series is cleared on every accepted input bar it delivers no bucket on —native_series_baranswersnulloptthere, the empty that stands forna— and carries the bucket only on the bars it publishes on. Which buckets complete, when they are delivered and what they contain are unchanged;on_native_timeframe_baris still called exactly once per delivered bucket and never for a cleared bar. The push side of a cleared bar isclear_security(sec_id), the counterpart of theevaluate_securitythe step dispatches; a bare host leaves both at their no-op defaults, a source host's reaches its generatedclear_security().
NativeTimeframeBarContext::completion is Confirmed when the bucket closed
on its own last contributing bar and LazyComplete when the next period's
first input closed it. interval is the bucket's own calendar interval — its
period at the subscription's timeframe, located by its first contributing bar;
delivered_at_ms is the input bar the delivery rides on. A bucket still open at the end of the input is never delivered.
A C host, whose on_timeframe_bar signature is frozen, reads that interval
with strategy_native_timeframe_bar_interval_v1 from inside the callback:
pf_native_timeframe_interval_v1 (open_ms, eligible_open_ms,
last_traded_close_ms, next_period_open_ms, next_input_open_ms) is a copy
of the kernel's interval, never inferred from the delivered bar or its
delivery time. Anywhere else the call answers PF_NATIVE_E_STATE.
Chronology against the script interval. The pump is ordered against the
script interval, not the raw input. With script_tf coarser than
input_tf, a script bar can still be open when the first input of a later
interval arrives: a session close clipped it short of its nominal end — the
last "60" bar of a 09:30–16:00 session, every "D" bar over an intraday
input — or the feed has a hole over its last slot. That input seals it
lazily (its calculation's coordinate says LazyComplete), and the kernel
runs, in this order and no other:
on_native_inputfor input i+1;- the lazily sealed calculation of script bar k, reading every series exactly as bar k's own contributing inputs left it;
- the deliveries riding on input i+1: a bucket it completes, a boundary it
closes, the clear of a
gapsseries it delivers nothing on, alookaheadbucket it opens; - input i+1's own aggregation, matching and calculation point.
seal(k) → deliver(i+1) → calc(i+1): a host never calculates a bar against a
bucket that already holds a later input. One consequence: a bucket the kernel
only learns is complete from input i+1 (LazyComplete) reaches the host
after bar k's calculation even when every one of its inputs lies inside
bar k — the same input that closes the bucket is the one that seals the bar.
That ordering matters only for a bare host that combines subscriptions with
a lazily sealed script interval. A run without subscriptions is untouched, and
so is every run whose input_tf equals its script_tf: each input seals its
own interval there, no script bar is ever sealed lazily, and the order is the
one under Delivery above. The Pine adapter is untouched as well: a
request.security site reaches the kernel's pump only at input_tf == script_tf, and on an aggregated chart the adapter keeps its own drive, which
already defers the next input past the lazily sealed calculation — this same
ordering. Pinned by the lazy-seal section of
tests/test_native_htf_subscriptions.cpp.
Authoritative bars. authoritative_bars are the exchange's own bars of
that timeframe. A completed bucket takes its OHLCV from the bar keyed to the
same period; the aggregator still decides when the bucket completes.
native_security_substitutions() and native_security_misses() report how
many completed buckets took one and how many found none.
Inherited TradingView calibration. The feed store these bars go into is TradingView-calibrated, and a host that supplies them inherits its rules:
- A
"W"/"M"series with no feed of its own is built from the installed daily bars of the same run — first session's daily open, the daily extremes, last session's daily close, summed volume — not from a re-aggregation of the intraday input. - An installed feed's stamps become that series' period partition. A session with no stamp of its own (an exchange holiday session that pauses and reopens the same day) therefore folds into the next trade date's bar, and a data hole inside a stamped period is not a close.
- A period the supplied bars only partly cover yields a partial bucket, exactly as a partly covered chart would.
- The installed daily bars also date where each day opened. A daily
stamp later than its session-day's day stamp and before its scheduled
close, with no input bar of that day before it, is a day that traded late,
and an intraday series lays that day's grid from the stamp: NSE's Muhurat
session of 2025-10-21 opens 13:45 IST, so its
"60"bucket is one bar stamped 13:45, not 13:15 and 14:15 from the 09:15 day stamp. Every other day keeps the day-stamp grid. - The installed daily bars also date the run's own
"D"script bars when an intraday input aggregates into them: each stamp labels the bar holding the session instant it covers (the stamp inside a session, else the next session's open), unless an input bar the run holds of that bar comes before it. OANDA stamps its XAUUSD daily bars at 17:00 ET, in the break of the 1800-1700 session, so that bar is labelled 17:00 ET rather than the session's 18:00 open, as TradingView dates it on a daily chart backtested with the bar magnifier. Only the label moves: the bar's span, closes and values stay the calendar's aggregate, a lookup by the label (the bar's open, its sub-bars, its session day) is the bar's own, a bar no stamp covers keeps the calendar's label, a label never passes an input bar the run holds, and a stamp at the calendar's own open changes nothing. Pinned bytests/test_native_day_labels.cppand, on TradingView's tapes,tests/test_daily_magnifier_tapes.cpp.
Declare no authoritative_bars and the buckets are a plain aggregation of the
run's own input, with no calibration to inherit. That is the whole policy
knob, and it is a ruling of record (ADR-0001, "TradingView-calibrated kernel
mechanisms", the authoritative-feed partition row): the rules above follow
from "the supplied
bars are the venue's own bars of that timeframe", so the kernel gates them on
the feed's presence rather than on a separate partition field.
One more rule holds with or without authoritative bars and is keyed by the
instrument class in spec.type: an early close (a session-day that ends
before its template's end) completes a calendar period for the exchange
classes ("stock", "futures", "index", "fund"), and does not for
the continuous-session classes "forex", "cfd" and "crypto", whose period
completes on the next session's first bar instead. The vocabulary is the one
the C ABI's strategy_set_syminfo_type fixes.
Every calendar series also closes each bucket once, and not while the next
input bar the run holds still belongs to its period. A session template that
knows Monday to Friday ends a week at Friday's close; an exchange that trades a
weekend session inside that week (NSE's Budget-day Sunday of 2026-02-01) keeps
the week open until that session's last bar, where the series advances one
bucket. A bar with no known successor -- a stream, the feed's last bar --
keeps the template's rule, and a later bar of an already closed period merges
into it without completing it again. Pinned by
tests/test_calendar_bucket_closes_once.cpp.
Declaring at begin. A host whose series are known only to its own
begin-time registration calls
declare_timeframe_subscriptions(std::vector<NativeTimeframeSubscription>)
from inside on_native_run_begin. The list replaces the staged spec's
subscriptions, so the staged spec keeps naming exactly what ran and the run's
continuation identity folds the declared series. The call is legal only there:
anywhere else, and for a list this run's input_tf would refuse (the same
validation configure_native applies), it stages nothing, changes nothing and
answers false. It is not virtual — the host calls the kernel here.
Those are two refusals and one bit, so the call has a second spelling that
names them: declare_timeframe_subscriptions_result(...) answers a
NativeSetupResult, exactly as configure_native answers the identical
validation. A judged list carries that validation's own first error and field
(SubscriptionFinerThanInput at SubscriptionTimeframe, and the rest of the
table under Validation (at configure_native) above); a call made anywhere
but inside
on_native_run_begin — or on a host that has already failed — judged no list
at all and is NativeRunSpecError::WrongPhase at NativeRunSpecField::None.
The bool spelling is ...status == NativeSetupStatus::Applied and nothing
else: same staging, same commands, same answer.
The kernel registers the declared series after on_native_run_begin
returns, which is what makes that hook usable: a host that registers its own
request.security evaluators there (clearing
BacktestEngine::security_eval_states_ first, as generated code does) can no
longer erase the kernel's registration, and the kernel appends its own states
after the host's rather than in place of them. The previous run's declared
series are torn down before that callback, so an evaluator state the
callback registers itself — even one identical to what the kernel registered
last run, as a host moving a series from the kernel's drive to its own does —
is the host's and stays. Two consequences worth stating: native_series_bar
answers nullopt for every index inside on_native_run_begin (nothing is
registered yet), and the kernel feeds only the evaluator states it registered
itself.
Feeds the host installed itself survive. set_native_security_feed — the
store the C ABI's strategy_set_native_security_feed writes — is host ingress
installed before a run. A later begin re-registers the kernel's own series and
removes only the feeds that registration installed; a feed the host put there
stays, and is simply replaced if a later begin declares authoritative_bars
of its own for the same period.
The Pine adapter on subscriptions. The source host runs its own
plain request.security sites through exactly this path. After generated
configure_security_evaluators() has registered them inside
on_native_run_begin, the adapter declares one series instance per site —
tf from the site, gaps from barmerge.gaps_on, lookahead off, no
authoritative_bars (a feed installed through set_native_security_feed
stays the store's) — through declare_timeframe_subscriptions, drops its own
registration, and the kernel registers the same sites sec_id by index and
steps them with its pump; the adapter's own pump feeds nothing for that run
(PineStrategyHost::declare_security_sites_to_kernel). The route is taken only
where the kernel step is the Pine step: a batch run with input_tf == script_tf, no auxiliary feed, no range-start warmup flag, no historical
lookahead projection, no forex/cfd intraday daily request (the OTC pins), and
every site lookahead_off, not Heikin-Ashi and not lower-timeframe. Every
other shape — streams (the kernel takes confirmed bars only), the bar
magnifier and aggregated charts (the calling-bar deferrals), lookahead_on
(per-input peeks and the merge latch), ticker.heikinashi, both
request.security_lower_tf paths, the auxiliary slice — keeps the adapter's
own drive, unchanged. The corpus' 23 request.security probes are all of the
first kind but the two lower-timeframe ones; their trades are byte-identical
on either drive, which is the parity evidence for the kernel pump. Every
condition of the predicate is pinned, with what it changes where it can be
tripped alone, by tests/test_adapter_security_route_conditions.cpp: on the
feeds measured the magnifier and an aggregated chart change no delivered
value (conservative exclusions, not calling-bar deferrals the kernel lacks),
while the range-start cut, the projection's incomplete tail, lookahead_on
and the OTC pins do; a registry whose sec_ids are not dense from 0 and a
pairing the kernel refuses at declare_timeframe_subscriptions keep the
evaluator too.
Streams. stream_begin accepts a non-empty subscriptions, so a
forward-execution host reads the same series a backtest of the same bars
reads. The warmup resolves the series exactly as a run() over those same
warmup bars does — the same buckets, at the same delivery points, in the same
callback order — and each pushed live bar then extends the bucket the warmup
left open, delivering it before the calculation of the bar that completed it.
Live specifics:
lookahead = truechanges the warmup only. Pine's lookahead is a historical-resolution mode, and a realtime bar has no future to resolve over, so a live bucket is delivered when it completes under both modes.- A calendar (
D/W/M) bucket closes on its period end or its session close, and a stream reaches both on its own: a session-clipped daily series over a warmup that stops mid-session is the batch's series bucket for bucket. The one completion rule a stream cannot use is the fallback that closes a period on its last input because the next input's stamp is already known — a batch has that stamp for every bar but its last, and a stream never has it for the bar it has just received. A period whose input simply stops early therefore waits for the next period's first pushed bar and arrives asLazyComplete. A session-clipped bucket is different: the calendar closes it on its own last contributing bar and delivers itConfirmed. authoritative_barsare installed once, at begin, and therefore cover the buckets the warmup completes. A live bucket with no authoritative bar of its own aggregates the pushed input and is counted bynative_security_misses().- A bucket still open when the stream ends is not delivered by
stream_end, exactly as a batch never delivers the input's trailing partial bucket. - A stream that declares a series takes confirmed bars only: tick input
and
stream_advance_timeare refused by name. An observed-tick slot is finalized after its own matching pass and a quiet-carried slot is a synthesized flat bar, so neither has a batch counterpart a bucket could be built from.
A spec that declares no series is untouched by all of this: its stream event
sequence, continuation hash and stream_state_hash() are the pre-subscription
ones.
Limits. A series finer than the input is refused at configure, not emulated, unless it is built from an auxiliary finer feed the host declares ("The auxiliary finer feed" below). Another symbol is addressable as data: a series reads another instrument's own bars from a feed the host installs ("Instrument feeds: another symbol's bars" below), merged by interval and aggregated by nothing. There is no chart-slice mapping.
A host whose input is 15-minute bars may also hold the 1-minute bars of the same symbol. It hands them to the kernel as the run's auxiliary feed and names, per series, which bars the series is built from:
NativeAuxiliaryFeed feed;
feed.tf = "1"; // strictly finer than input_tf
feed.bars = one_minute_bars; // strictly increasing; may be empty
spec.auxiliary_feed = feed;
NativeTimeframeSubscription five;
five.tf = "5"; // finer than the 15-minute input
five.source = NativeSeriesSource::AuxiliaryFeed;
spec.subscriptions.push_back(five);auxiliary_feed is absent by default and source defaults to
NativeSeriesSource::Input, so every part of this section is inert for a spec
that uses neither: nothing is stored, nothing is routed, and both the series
digest and the run-spec digest are the ones they were before the fields
existed (each folds only where a host opted in). The feed drives nothing by
itself — no matching point, no calculation, no script bar; it is read only by
the series that name it.
A series built from the feed pairs with the feed's timeframe exactly as
script_tf pairs with input_tf, so it may be finer than the input ("5"
over a "15" input and a "1" feed), equal to it, or coarser ("60", "D")
— and a series of the feed's own timeframe passes every feed bar through.
lookahead, gaps, authoritative_bars, the series-instance rule, the
callback, the pull accessor and the chronology against the script interval
are all as above; a series still built from the input is unchanged by the
feed's presence, SubscriptionFinerThanInput included.
Routing is by time, and by nothing else. When an input bar is accepted,
every feed bar not yet consumed that opened before that input's period
ended (NativeInterval::next_period_open_ms of the input's own interval) is
folded, in feed order, into each AuxiliaryFeed series, at the delivery point
every series has: after on_native_input, before that input is aggregated,
matched or calculated. So:
- bars inside an input ride on it, and a series finer than the input delivers
several buckets on one input, oldest first (
NativeTimeframeBarContext:: intervalis then the bucket's own span, read over the feed's timeframe, anddelivered_at_msstays the input bar); - bars in a hole of the input ride on the next accepted input;
- bars earlier than the first input are folded on that first input — this is how a host supplies history the input does not reach, and a bucket that history completes is delivered there, ahead of the input's own;
- bars later than the last input's period are never folded;
- a bucket the feed leaves short is learned complete only from the next feed
bar (
LazyComplete), which rides on a later input.
Under lookahead = true a bucket's final values are delivered on the input
whose slice held its first feed bar. Deliveries riding on one input arrive
series by series in declaration order, each series' buckets oldest first.
Validation. At configure_native, and by the same functions at begin
(validate_native_auxiliary_feed, and
validate_native_timeframe_subscriptions with the feed as its fourth
argument):
NativeRunSpecError |
Cause |
|---|---|
InvalidAuxiliaryFeedTimeframe |
unparseable literal, or a pairing under input_tf an input would not accept under a script_tf ("10" under "15") |
AuxiliaryFeedNotFinerThanInput |
the input's own period, or coarser |
UnorderedAuxiliaryFeedBars |
bars not strictly increasing in time |
InvalidAuxiliaryFeedBar |
a bar native_bar_structurally_valid refuses (non-finite or non-positive price, low/high not bracketing open/close, negative volume) |
AuxiliaryFeedWithoutTimeframe |
declared together with timeframe_undetected |
Declaring at begin. declare_auxiliary_feed(std::optional< NativeAuxiliaryFeed>) is the feed's counterpart of
declare_timeframe_subscriptions: legal only inside on_native_run_begin,
replacing the staged spec's feed (nullopt withdraws it) before the kernel
registers, so the staged spec names what ran and the continuation identity
folds it. The feed is judged together with the series staged at that moment —
a host that names both at begin declares the feed first and its series
second — and a declaration that would leave a staged AuxiliaryFeed series
without its bars, or finer than them, changes nothing and answers false.
declare_auxiliary_feed_result(...) is the same call answering a
NativeSetupResult: AuxiliaryFeedNotFinerThanInput at
AuxiliaryFeedTimeframe for a feed the input refuses,
SubscriptionWithoutAuxiliaryFeed at SubscriptionSource for a withdrawal
that would strand a staged series, and WrongPhase at None for a call made
outside on_native_run_begin. The bool is that result's status.
Streams. The begin-time feed covers what the host knows then; the warmup
resolves its series exactly as a run() over the same inputs and the same
feed does. Live, the host appends each input's finer bars and then pushes the
input:
host.append_auxiliary_bars(finer.data(), finer.size()); // the 15 one-minute bars
host.stream_push_bar(quarter_hour_bar); // the input they belong toAppended bars join the feed behind every bar it holds and are routed by the
very rule above, so a stream fed this way reads the batch's series, bucket for
bucket and delivery point for delivery point
(tests/test_native_auxiliary_feed_stream.cpp). Bars the begin-time feed
already holds beyond the warmup are consumed by the live inputs the same way.
append_auxiliary_bars refuses by name, changing nothing and without
failing the host: a run that is not realtime, a run that declared no feed,
bars out of order or not after the feed's last bar, a bar with invalid OHLCV,
and a bar that opened inside an input period already accepted — its slice is
closed, and folding it late would build a series no batch of the same bars
could. Called from inside a callback it is the contract failure every
reentrant stream input is.
Those names are values, not just text: append_auxiliary_bars_result(...)
answers a NativeAuxiliaryAppendResult — a NativeSetupStatus, a
NativeAuxiliaryAppendError (NotRealtime, NoAuxiliaryFeed,
InvalidBarArray, InvalidBar, UnorderedBars,
InputPeriodAlreadyAccepted, plus HostFailed and Reentrant for the two
legality refusals every public stream input shares, and AllocationFailure)
and the index of the bar of that call it stopped on. last_error() is
the presentation text beside it, unchanged, and the bool spelling is that
result's status. Appended bars are durable input and are folded
into the continuation hash (count and running content digest), as is each
feed-built series' cursor; the one completion rule a stream cannot use —
closing a period on its last bar because the next bar's stamp is already
known — applies to the feed's own successor exactly as it does to the input's.
What this is not. It is not the Pine source host's split-feed path.
BacktestEngine::set_aux_security_feed — the C ABI's
strategy_set_aux_security_feed — stays that host's door: on a bare host it
answers false, exactly as before, and the generic door is
NativeRunSpec::auxiliary_feed. TradingView's
chart-slice mapping (chart bars re-keyed by the session they cover, calendar
charts routed by their actual stamps, pre-range coverage left inert, the
range-start cut), the calling-bar completions and the deferred first-bucket
publication are state and code of source::PineStrategyHost
(src/source/pine_aux_security.cpp) and are not reachable from a native host.
The Pine adapter keeps its own auxiliary drive — retained, measured. The adapter's plain sites already run on kernel subscriptions where the kernel step is the Pine step ("The Pine adapter on subscriptions" above), and an auxiliary feed is one of the shapes that route excludes. It stays excluded, on three measurements:
- No population can witness the move. 0 of the corpus' 312 probes install
an auxiliary feed (
git -C corpus grep -l aux_securityis empty at the pinned corpus), so whole-corpus byte-identity says nothing about a re-lowered auxiliary path either way. - The slice is not the generic one. TradingView leaves feed coverage
before the first chart bar inert and cuts the bucket in progress at the
range start; the kernel routes by time and folds that history on the first
input. Over one feed with an hour of history the adapter publishes two
hourly buckets and a bare host three
(
tests/test_native_auxiliary_feed_twin.cpp, row B) — and every split feed the adapter serves carries such history. Under an auxiliary feed every site finer than the chart additionally carries a Pine-only completion rule (calling_close_completes_partial/calling_open_latches_first), set unconditionally at validation. - The evaluation point differs on every shape, the congruent one
included. The adapter publishes a chart bar's slice at that bar's
calculation, after the bar's own matching pass; the kernel delivers a
series on the accepted input, before it is matched. With a position
opened at bar 1's open, the adapter evaluates bar 1's bucket against a
position of 1 and the kernel delivers it against 0 (same twin, row C). A
generated
evaluate_securitybody is opaque to any routing predicate, so no predicate can prove that move neutral.
On the congruent shape — a feed that begins at the first chart bar, a plain
lookahead_off site no finer than the chart — the two series are the same
buckets read at the same calculations (row A): the generic feed can express
it. Re-lowering it waits on a population that exercises it.
What the kernel keeps, and what it does not. The machinery a subscription
runs on is the kernel's and is generic: the evaluator registry
(BacktestEngine::SecurityEvalState — series id, timeframe, its
TimeframeAggregator, the current bucket, the feed / evaluation counters and
the native-feed routing), register_security_eval(sec_id, tf, input_tf), the
feed store (set_native_security_feed, prepare_native_security_feeds) and
one evaluator step, feed_security_eval_state(state, bar): aggregate the
input, give a completed bucket the authoritative bar keyed to its period, and
publish it. That step has no publication modes — lookahead and gaps above
are the consumer's delivery rules, applied around it.
TradingView's request.security semantics are not in the kernel. The
barmerge.lookahead_on merge latch, barmerge.gaps_on and the
calling-bar publication gates, ticker.heikinashi substitution, the KI-55
range-start cut with its OTC-daily pins, the historical lookahead projection,
request.security_lower_tf emulation, the deferred auxiliary chart slice and
the validate_security_timeframes diagnostics are state and code of the Pine
source host (source::PineStrategyHost, src/source/pine_security_eval.cpp):
a per-sec_id table, source::PineSecurityEvalState, kept beside the generic
state of the same id and folded into the source hash extension under its own
domain (pineforge-source-security/vN, only when a site is registered). For
the sites that need those rules the source host composes its own evaluator
step from the same kernel primitives and the kernel never calls into it; its
plain sites it declares as subscriptions ("The Pine adapter on subscriptions"
above), and the kernel's step is then the only step. A native host therefore
cannot reach any of those rules, and none of them can change a subscription's
buckets.
A host whose chart is one instrument may read another's bars: a dollar index beside a stock, a yield beside a future. It installs the other instrument's own bars as data and names, per series, which instrument the series reads:
NativeInstrumentFeed dxy;
dxy.instrument = "TVC:DXY"; // an opaque key, compared byte for byte
dxy.tf = "15"; // part of the key; it pairs with nothing
dxy.bars = dxy_bars; // strictly increasing opens
dxy.close_ms = dxy_closes; // each bar's own close
spec.instrument_feeds.push_back(dxy);
NativeTimeframeSubscription series;
series.tf = "15";
series.source = NativeSeriesSource::InstrumentFeed;
series.instrument = "TVC:DXY";
spec.subscriptions.push_back(series);instrument_feeds is empty by default and instrument is empty on every other
series; the run-spec digest folds the feeds only when a host installed one, so
every spec that uses neither keeps the digest it had. A feed drives nothing by
itself -- no matching point, no calculation, no script bar -- and the kernel
guesses no calendar for the other instrument: a bar opens at its timestamp and
closes at close_ms[i], as the feed says (a 240-minute bar its session clips at
13:26Z closes at 13:26Z). A feed may carry named columns, one value per bar (a
footprint delta); the kernel validates and digests them and reads nothing in
them.
Delivery is by interval, and by nothing else. On each accepted input, every
bar of the feed not yet handed to the series that is visible at that input is
handed over, in feed order, at the delivery point every series has (after
on_native_input, before the input is aggregated, matched or calculated):
lookahead = false: bars whoseclose_msis at or before the input'sNativeInterval::last_traded_close_ms-- the bars that have closed by the time the input has;lookahead = true: bars whose open is at or before the input'sNativeInterval::open_ms-- the bar in progress at the input's open included, with the final values the feed gives it.
The last bar handed over is the series' value (native_series_bar()), each
delivery reaches on_native_timeframe_bar, and every bar is handed over exactly
once, in order, so a host that counts deliveries knows each bar's feed index,
and so its column values. Bars earlier than the first input ride on that first
input (history); bars the feed carries past the last input's horizon are never
handed over. gaps is the subscription contract's: true clears the series on
every accepted input that hands it nothing. The callback's interval is the
bar's own -- open and eligible open its timestamp, last traded close and next
period open its close_ms, next input open the next feed bar's open -- and its
completion is Confirmed. The input's interval is its calendar interval: on the
tolerant raw-label ingress, whose inputs carry no duration, it is the interval
the run's session, zone and input timeframe give the input's label.
Two consequences, both pinned on synthetic bars by
tests/test_native_instrument_feed.cpp in the shapes XSYM-DESIGN measured
against TradingView: a daily bar of another instrument that closes after the
chart's session close is seen one chart day late, and a weekday-only feed is
carried over a 24x7 chart's weekend under gaps = false and reads empty there
under gaps = true.
Validation. At configure_native, and by the same functions at begin
(validate_native_instrument_feeds, and validate_native_timeframe_subscriptions
with the feeds as its fifth argument):
NativeRunSpecError |
Cause |
|---|---|
EmptyRequiredString |
an empty instrument key, on a feed (InstrumentFeedInstrument) or on an InstrumentFeed series (SubscriptionInstrument) |
InvalidInstrumentFeedTimeframe |
the feed's literal does not parse |
DuplicateInstrumentFeed |
two feeds with one (instrument, timeframe) key; "D" and "1D" are one key |
UnorderedInstrumentFeedBars |
opens that are not strictly increasing |
InvalidInstrumentFeedBar |
a non-finite price, or a volume that is neither NaN nor finite and nonnegative; a zero or negative price is admitted, since the kernel trades nothing on the feed |
InvalidInstrumentFeedClose |
close_ms of another length than the bars, a close at or before its bar's open, or one after the next bar's open |
InvalidInstrumentFeedColumn |
an empty or repeated column name, or a column of another length than the bars |
SubscriptionWithoutInstrumentFeed |
an InstrumentFeed series naming an (instrument, tf) no feed installs |
InstrumentSubscriptionBars |
an InstrumentFeed series that declares authoritative_bars |
InstrumentOnNonInstrumentSeries |
an instrument key on an Input or AuxiliaryFeed series |
Streams refuse them. A feed is installed whole at begin and has no realtime
ingress, so stream_begin refuses a spec that installs one ("native instrument
feeds are not supported by streaming") and leaves the host Ready. C has no
spelling for either field (the 1.0 C boundary table's "instrument feeds" row).
The Pine adapter on instrument feeds. A generated request.security of
another symbol registers its site with the symbol-key overload
register_security_eval(sec_id, symbol, tf, input_tf, lookahead_on, gaps_on, ignore_invalid_symbol) (PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1), and reads
the bars its host was handed through strategy_set_symbol_feed,
strategy_set_symbol_feed_column and strategy_set_symbol_facts
(pineforge.h). The source host projects those feeds as the run's
instrument_feeds and declares one InstrumentFeed series per site at the
begin-time hook, after its same-symbol sites, which keep their own route. The
kernel merges; the adapter runs the site's payload on every bar handed over, in
the requested context: history offsets and TA state over the context's own
bars, bar_index the context's, time_close the bar's own close, syminfo.*
the symbol's facts (syminfo.tickerid is the string the script passed). On a
D, W or M chart the adapter judges TradingView's merge against the chart bar's
own period: after the input's deliveries it runs the bars closed by the chart
bar's time_close (lookahead off), or opened by its time (lookahead on, an
intraday requested timeframe), that the kernel has not handed over yet, and a
bar the kernel hands over later is not run again. The two agree wherever the
chart bar's stamp opens its own session; OANDA stamps its XAUUSD daily bars at
17:00 ET inside the 1800-1700 session's break, and the kernel reads such a
label as the session that closes at it (tests/test_foreign_break_stamp_tapes.cpp).
barmerge.gaps_on reads na on a chart bar that received no new bar (through
the generated clear_security(), which also clears the payload's own history
series, as for a same-symbol site). A symbol whose facts say it is invalid
reads na under ignore_invalid_symbol and fails the run closed without it; a
registered symbol with no installed feed fails the run closed, naming the
symbol and the timeframe, while a feed installed with no bars reads na; a chart
aggregated from finer input is refused, since the merge is judged per chart
bar. Recorded request series -- TradingView's own per-chart-bar values of
request.earnings, dividends, splits and financial, installed by
strategy_set_recorded_series -- are the adapter's alone:
recorded_series_value(key) reads the value recorded for the chart bar's open
time, na where the tape has no row (on every bar for a tape installed without
rows), and a key nobody installed fails the read closed. stream_begin refuses
a source host holding a symbol feed or a recorded series ("request.security
symbol feeds and recorded request series support historical runs only"), and
each of the four setters answers -1 while a run is in progress. scripts/run_strategy.py installs all of it
from the probe's pinned requests manifest when PINEFORGE_REQUESTS_ROOT is set
(<root>/<probe>/requests.json, schema pineforge-probe-requests/v1, each
file it names under <root>/<probe>/files/<sha256>), and records
each feed's key, sha256 and bar count in the run provenance.
A host that wants only the running aggregate — no series instance, no
gaps/lookahead rule, nothing folded into the continuation identity —
aggregates the input itself. TimeframeAggregator (timeframe.hpp:321) is
public and engine-free; feed it from on_native_input, which runs once per
accepted confirmed input bar before that bar is aggregated or matched
(on_native_input native_host.hpp:875). Include
<pineforge/timeframe.hpp>:
class Htf final : public pineforge::NativeStrategyHost {
pineforge::TimeframeAggregator daily_{"D", "15"}; // script_tf, input_tf
std::optional<pineforge::Bar> last_daily_;
void on_native_input(const pineforge::Bar& bar,
const pineforge::NativeInputContext&) override {
const pineforge::AggregatedBar aggregate = daily_.feed(bar);
if (aggregate.is_complete) last_daily_ = aggregate.bar;
}
void on_native_bar(const pineforge::Bar&,
const pineforge::NativeDecisionContext&) override {
if (!last_daily_) return; // confirmed-only HTF value
// ... submit against last_daily_->close ...
}
};Only completed buckets are published, so this recipe has no lookahead by
construction. It is the same class the kernel's own subscription evaluator
aggregates with, and the one the kernel's script_bucket_completions query
feeds when the Pine scheduler asks how its input span buckets
(TimeframeAggregator native_execution_consumer.cpp:8850). What it does
not give you is what a
declared subscription does: an authoritative_bars feed, the gaps and
lookahead delivery rules, the lazy-seal chronology, a C spelling, and the
series' place in the run's continuous identity. Prefer subscriptions unless
you want none of those.
set_native_security_feed (engine.hpp:1731) is the host ingress for
authoritative_bars installed before a run — see Authoritative bars above —
and not a way to register a series: registration is
NativeRunSpec::subscriptions or declare_timeframe_subscriptions. In-run the
setter is a source mutation and throws, latching Failed
(UnsupportedSource) through guard_native_mutation
(engine_aux_security.cpp:83, guard_native_mutation engine_consumer.cpp:42).
Two driver models only: confirmed OHLCV and observed ticks. Mixing them on
one stream is refused. The script-bar calculation callback is
on_native_bar, one per completed script bucket; the opening, tick and
post-fill hooks fire in addition to it, not instead of it. Calculation is
close-only unless the spec asks otherwise; see Calculation timing above
for BarCloseAndFills and EveryModeledPoint.
Confirmed OHLC retains the modeled Opening, high/low in the existing AUTO order, close, calculation and optional AfterCalculation close sequence. Default market requests match at the modeled opening and eligible AfterCalculation close; high/low/close traversal updates their excursions. Resting requests additionally match along the remaining continuous modeled price segments. The earliest eligible hit is resolved before later hits; trigger activation, partial fills and newly eligible relationships retain their causal cursor. Geometric fractions keep the original point's time and identity rather than inventing intermediate timestamps.
Observed tick: the real price at its timestamp/sequence (match / excursion). Sequence, when nonzero, must increase. Off-session observed prints are still delivered and may fill an eligible live request. Resting triggers evaluate each actual print without interpolating between prints.
Quiet tradable interval with no prints: explicit carried open at that interval’s tradable open, last known price (match if eligible), then calculation if the script bucket completes. Configured closed spans synthesize neither bars nor matching points.
stream_advance_time(ms) finishes elapsed intervals and raises the accepted
time floor; it is not an extra print. stream_end(false) completes without
inventing a final callback or fill. stream_end(true) may partial-finalize
an existing forming observed input and calculates a pending script bucket
when its final contributor reached the calendar's last-traded close. It does
not synthesize a missing input or complete a bucket whose session is still open.
The decision floor is monotonic (delivered event time, accepted input completion, tick time, time-advance). A refused preflight does not raise it. Birth compares to that immutable floor, not to a later lowered clock.
These are existing refusals, not implied future features:
- Monthly input (
M/nM) onstream_begin/ runner native warmup - Mixed confirmed bars and ticks
- In-session gaps on a realtime stream, and in a
Canonicalstream warmup - Source
calc_on_every_tick/calc_on_order_fillsenabled (the runner rejects an explicit true override, and the Pine host refuses a stream begin withcalc_on_order_fills,pine_strategy_host.cpp:278-281). This is a source-route refusal, not a limit on the native hooks:on_native_tickandon_native_appliedare delivered on a stream, and a native host's ownNativeRunSpec::calculationis accepted there, whereEveryModeledPointrecalculates per observed print - Tick-driven input (
stream_push_tick/stream_push_ticks/stream_advance_time) on a stream that declares aNativeFxCurve; confirmed bars run under one (see @ref native_engine_stream_fx) - A timestamped FX series staged through the mutable setter ingress
(
set_account_currency_fx_series) onstream_begin; its owner revalues on a broker clock of its own, which has no realtime route - Auxiliary/native security feeds, source magnifier/tail/probe/hash/trace
setters,
set_input, and Pine entry/exit/cancel commands — native hosts latchFailed(UnsupportedSource) before mutation - Tick input (
stream_push_tick/stream_push_ticks/stream_advance_time) on a stream whose spec declaressubscriptions; confirmed bars carry those series - A spec that installs
instrument_feeds(installed whole at begin, with no realtime ingress), and a Pine source host holding a symbol feed or a recorded request series
A C host has the same stream and the same commands. Streaming needs no new
symbol — strategy_stream_begin and its family (pineforge.h:739) take
a pf_strategy_t from strategy_native_host_create_v1 unchanged — and
strategy_native_submit_v1 (native_c_api.h:2742) obeys the one legality
rule its C++ spelling does.
Rebuild strategy libraries against this engine. An ABI-v4 module without the
native contract is legacy and cannot take --native-config.
| Situation | Host state | What to read |
|---|---|---|
| Invalid spec at configure | Failed (InvalidSpecification) |
native_state().failure, last_error() |
Configure while Ready / Running; session-key change; run number ≤ high-water |
Failed (Contract) |
same |
| Timeframe args ≠ spec; unaligned/invalid bars; in-session stream gap; monthly stream input | Not Failed if still Ready/Running |
last_error(); run leaves Ready; stream_* returns false |
| Source command/setter during a run (before a run, a setter is staged) | Failed (UnsupportedSource) |
discard host |
Exception from on_native_bar / on_native_run_begin |
Failed (CallbackException) |
discard host |
| Unexpected settlement/allocation/counter exhaustion after begin | Failed |
discard host; no rollback promise |
| Submit/replace/cancel outside the allowed phase | throws; not a fill | command only from callbacks or between realtime inputs |
| Cooperative abort | Failed (Aborted) |
reuse with configure_native using the same session key and a higher run number |
last_run_status() is not failure authority. After a cooperative Aborted
failure, configure_native can reuse the host with the same session key and
a higher run number. Other failures remain latched: subsequent run,
stream_* and configure_native calls refuse (native host already failed).
Complete host: fill every NativeRunSpec default that validation requires,
submit Transact / Flatten from the callback, query physical_position().
Five UTC 24x7 five-minute bars, Unix ms, positive finite OHLC.
#include <pineforge/native_host.hpp>
#include <iostream>
#include <variant>
namespace {
class NativeMarketExample final : public pineforge::NativeStrategyHost {
int bars_ = 0;
void on_native_run_begin() override { bars_ = 0; }
void on_native_bar(const pineforge::Bar&,
const pineforge::NativeDecisionContext&) override {
++bars_;
if (bars_ == 1 && physical_position().signed_units == 0.0) {
submit_market({pineforge::order_action::Transact{1.0}, "native-open", ""});
} else if (bars_ == 4 && physical_position().signed_units != 0.0) {
submit_market({pineforge::execution::Flatten{}, "native-flat", ""});
}
}
};
pineforge::NativeRunSpec make_spec() {
pineforge::NativeRunSpec spec;
spec.identity.session_key = "native-guide";
spec.identity.run_number = 1;
spec.input_tf = "5";
spec.script_tf = "5";
spec.ticker = "MOCK";
spec.tickerid = "TEST:MOCK";
spec.type = "crypto";
spec.currency = "USDT";
spec.basecurrency = "ETH";
spec.description = "";
spec.volumetype = "";
spec.timezone = "UTC";
spec.session = "24x7";
spec.chart_timezone = "";
spec.initial_capital = 10000.0;
spec.point_value = 1.0;
spec.account_fx = 1.0;
spec.price_tick = 0.01;
spec.slippage_ticks = 0;
spec.fee_kind = pineforge::NativeFeeKind::CashPerExecution;
spec.fee_value = 6.0;
spec.close_execution = pineforge::NativeCloseExecution::NextEligiblePoint;
spec.allowed_open_directions = pineforge::NativeOpenDirections::Both;
spec.event_retention = pineforge::NativeEventRetention::Full;
return spec;
}
const pineforge::Bar kBars[] = {
{100.0, 102.0, 99.0, 101.0, 4.0, 0},
{102.0, 103.0, 101.0, 102.0, 4.0, 300000},
{103.0, 104.0, 102.0, 103.5, 4.0, 600000},
{103.5, 104.0, 103.0, 103.5, 4.0, 900000},
{104.0, 105.0, 103.5, 104.0, 4.0, 1200000},
};
constexpr int kBarCount = 5;
bool failed(const pineforge::NativeStrategyHost& host, const char* where) {
const auto state = host.native_state();
if (state.kind == pineforge::NativeLifecycleKind::Failed) {
std::cerr << where << ": Failed code="
<< static_cast<int>(state.failure.code)
<< " op=" << static_cast<int>(state.failure.operation)
<< " " << host.last_error() << '\n';
return true;
}
return false;
}
} // namespace
int main() {
const auto spec = make_spec();
NativeMarketExample batch;
const auto setup = batch.configure_native(spec);
if (setup.status != pineforge::NativeSetupStatus::Applied) {
std::cerr << "configure: " << batch.last_error() << '\n';
return 1;
}
batch.run(kBars, kBarCount);
if (failed(batch, "run()")) return 1;
if (batch.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
std::cerr << "run() not completed: " << batch.last_error() << '\n';
return 1;
}
NativeMarketExample batch_tf;
if (batch_tf.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
return 1;
batch_tf.run(kBars, kBarCount, "5", "5");
if (failed(batch_tf, "run(tf)") ||
batch_tf.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
std::cerr << "run(tf) " << batch_tf.last_error() << '\n';
return 1;
}
bool accepted = false;
bool filled = false;
for (const auto& event : batch.native_events(0)) {
if (!event.command) continue;
if (std::holds_alternative<pineforge::native_order::AcceptedEvent>(*event.command))
accepted = true;
if (std::holds_alternative<pineforge::native_order::ExecutionAppliedEvent>(*event.command))
filled = true;
}
if (!accepted || !filled) {
std::cerr << "expected AcceptedEvent and ExecutionAppliedEvent\n";
return 1;
}
if (batch.physical_position().signed_units != 0.0) {
std::cerr << "expected flat book after flatten fill\n";
return 1;
}
NativeMarketExample stream;
if (stream.configure_native(spec).status != pineforge::NativeSetupStatus::Applied)
return 1;
if (!stream.stream_begin(kBars, 1, "5", "5")) {
std::cerr << "stream_begin: " << stream.last_error() << '\n';
return 1;
}
for (int i = 1; i < kBarCount; ++i) {
if (!stream.stream_push_bar(kBars[i])) {
std::cerr << "stream_push_bar: " << stream.last_error() << '\n';
return 1;
}
}
if (!stream.stream_end()) {
std::cerr << "stream_end: " << stream.last_error() << '\n';
return 1;
}
if (failed(stream, "stream") ||
stream.native_state().kind != pineforge::NativeLifecycleKind::Completed) {
std::cerr << "stream not completed: " << stream.last_error() << '\n';
return 1;
}
return 0;
}Intended path on this equal-TF feed, NextEligiblePoint: callback 1 accepts
Transact{+1} (no fill yet); the next bar’s modeled open is the fill;
callback 4 accepts Flatten while long; the next open closes. Acceptance
events and ExecutionAppliedEvent are separate rows in native_events.
Empty timeframe strings are also valid (run(bars, n) and
stream_begin(bars, n, "", "")). "5" / "5" must match the spec bytes.
The two const host hooks are present at the current host epoch,
engine_script_run_v19 (native_host.hpp:20).
resolve_execution_terms sees read-only
candidate facts and returns a resolved price plus units for an unresolved
request: required for HostSized, optional for Sized and ScopeFraction,
whose units the kernel resolves first (see Sizing without a host override). Its default is the identity price with no units. A
NativePrecommitView is then available to
validate_execution_precommit after the ordinary execution is prepared and
before any physical effect; returning Refuse records a nonfinancial
HostPrecommit rejection. The validator runs once only for an Applied-ready
physical attempt and never during inspect_current_execution.
ReverseTo names an exact signed target exposure. Its receipt records target
units separately from filled_working turnover, so a reversal remains one
ticket and one settlement cycle. A HostSized{Open} is sized once; later
candidate rematches may re-resolve price but not size. CloseOpposite uses the
existing whole-book Flatten path when it must close an absorbed roster.
A Transact that crosses the book (it closes the opposite position and opens
the rest on its own side) is charged exactly its units. The settlement
computes the opening in binary64 as the units minus the closed part, and
closed + |opened| can round one ulp either side of the units; the fill's
filled_working is the request's units, so the one fill finishes it (R5 lane
B-ENGINE). Before, a sum one ulp above stopped the run, and one ulp below left
a 2^-49-unit remainder that a second fill opened as a dust lot.
Every quantity request ends in exactly one terminal outcome, and no ordinary
quantity stops the run, nor does a sibling's share of it (last below). The
FIFO rules are the settlement's (docs/native-settlement.md, "Physical lots
and quantities"); what a host sees:
- A close that spans lots and ends inside one is one terminal fill whose
closed_unitsandfilled_workingare its request, even where the rows, the whole lots and the rest, add up one ulp away (R5 lane K-ULP2). - A close whose binary64 FIFO sum reaches its request at a lot -- the book's own held total, a FIFO prefix of it -- closes that lot whole and leaves no dust lot (K-ULP3).
- A crossing
Transactis charged exactly its units (K-ULP1, above). - A request the settlement cannot book exactly on the book it meets is refused,
typed: a terminal
MatchRejectedEventwithMatchRejectReason::UnrepresentableQuantity(C:PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY), nothing moved, the run still running (R5 lane K-ULP4).inspect_current_executionshows the settlement's refusal beforehand as asettlement_readinessofUnrepresentableQuantity(the request core's check and the grid's boundary re-check are made at execution only), andexecute_currentanswers theMatchRejectedEventwithout throwing. Ordinary decimal quantities reach it: after open 1.5,Reduce1.3 and open 2.9, aReduce 0.2needs2^-54of the 2.9 lot, and a dust lot a close left at the head of the book refuses the next close and the next opening on its side. Until K-ULP4 each of these stopped the run with code 6, discriminator 5.Flattenis never refused for a quantity. - On a
quantity_gridthe book's own quantities are on the grid: aScopeFractionwhose product is its scope's held total as it stands (fraction == 1of the gross scope, settled in one fill) resolves to that total, unfloored, and aReduceof a FIFO boundary of its scope is admitted (above). A fill the request's own units cannot absorb -- a scope of dust closed by a far larger request -- is the same typed refusal, from the request core's check. NativeRunSpec::quantity_toleranceis the opt-in for a host that keeps decimal quantities, as a broker ledger does: within the tolerance a close ends at the FIFO boundary it is near, charged its request, and a dust lot a close reaches closes whole, so such a host's closes neither leave dust nor meet the refusal above. A C host sets it in thequantity_tolerancetail ofpf_native_run_spec_ext_v1. The Pine adapter does not declare it and keeps its own1e-10rule (docs/native-settlement.md, "Quantity tolerance").- A deduction an OCA-Reduce sibling cannot take is absorbed, and the run goes
on (R5 lane K-ULP5). In a group with
GroupEffect::Reducea member's fill is deducted from every live sibling: from its remaining units (aReservationReducedEvent; CPF_NATIVE_EVENT_RESERVATION_REDUCED), or, while its units are not known, into its pending total (aDeferredGroupAdjustmentEvent; CPF_NATIVE_EVENT_DEFERRED_GROUP), which its terms (TermsResolvedEvent) or its owner's fill (QuantityBoundEvent) later takes off the units they bind. A deduction too small to move what it is taken from --fl(units - d)is the units again, at most half an ulp of them -- leaves the sibling's units, or its pending total, as they were, the exact binary64 result; the member's fill stands, and the event says it took nothing:actual_deduction0 withafterequal tobefore, ordeferred_delta0 withpending_afterequal topending_before(it joins no chain), oreffective_deduction0 beside a positivepending_totaland units above zero that stand. A C host reads the first two asPF_NATIVE_EVENT_RESERVATION_REDUCEDandPF_NATIVE_EVENT_DEFERRED_GROUProws whoseclosed_unitsis 0; itsTERMS_RESOLVEDandQUANTITY_BOUNDrows carry no deduction, so there it reads the absorption as the units that stand -- the fill, or the working row's remaining units -- beside theDEFERRED_GROUProws it received. Each deduction is taken on its own, so fills absorbed one by one can leave a sibling above what one deduction of their sum would. After open 0.5, open 1000.1 and a bound close of 1000, one group of a restingReduce 1000, aReduce 0.3and aScopeFraction 1: the fraction closes 2.2759572004815709e-14, below half an ulp of the resting member's 999.7, which keeps 999.70000000000005. A pending total below half an ulp of a later fill rounds into it, as binary64 addition does. Until K-ULP5 each of these failed the run after the fill was booked, with code 6, discriminator 7 (CoreFailure::UnrepresentableReservation); a pending total that overflows binary64 -- fills near the largest finite double -- still does.
native_order::Sized is the sixth OrderIntent: an opening whose size the
kernel resolves, so a bare host needs no resolve_execution_terms override to
buy a cash amount or a share of equity.
no::Request request;
request.intent = no::Sized{no::Side::Long, no::EquityFraction{0.10}};
submit(request); // 10 % of marked equitySizeBasis is CashValue{cash} in account currency or EquityFraction{f}, a
fraction in (0, 1] of marked equity at the sizing point. Resolution is
units = cash / (price * point_value * fx), where price is the sizing-point
price. SizeTime::AtMatch (the default) resolves at the matching candidate;
SizeTime::AtAcceptance freezes the units against the command point when the
request is accepted. reserve_percent_fee divides the
sizing cash by 1 + fee_value / 100 when the run's fee kind is
NativeFeeKind::Percent, and is an exact no-op for every other fee kind.
NativeRunSpec::fee_value is a percent, exactly as the charge reads it
(calc_commission bills fee_value / 100 of the account notional), so a
0.1 % fee reserves 0.1 % of the sizing cash. grid_policy reuses
ExecutionGridPolicy: SnapToGrid floors the units onto the run's
quantity_grid; ExplicitUnits keeps the literal quotient, which the ordinary
on-grid terms gate then refuses as InvalidTerms when a grid is configured and
the quotient is off it. The two are identical on an ungridded run.
The grid floor is the largest grid multiple at or below the quotient: it never
lands above it, and a quotient of at least one whole step is never refused for
being off the grid. Its only tolerance is the engine's own on-grid predicate
(quantity_on_grid, about four ulp and strictly inside half a step), which
keeps an already-on-grid quotient in its own binary64 representation instead of
rebuilding it one ulp away. No proportional epsilon is applied: a floor that
rounded 2.9999995 up to a full three units on a one-unit grid would book a
quantity the basis cannot fund.
The kernel resolves the basis before the host hook and publishes the result
as the facts' RemainingUnits, so a resolve_execution_terms override still
has the last word: return your own units to replace the kernel's, or the
default identity terms to accept them. That holds for both sizing points:
an AtAcceptance request reaches the candidate with a deferred quantity
carrying its frozen number, so there is exactly one terms pass in which a host
quirk — a money band, an affordability rule, a floor of its own — sees the
kernel's quantity and may answer with a different one. A basis that is not
representable — not finite, not positive, or below one grid step after
snapping — is MatchRejectReason::TermsUnresolved at the candidate. An invalid
basis (fraction outside (0, 1], cash <= 0) is
RequestRejectReason::InvalidQuantityBasis at submit. Sized requires the
Independent owner.
A kernel-sized opening settles as a signed book transaction on its declared
side, or through the ReverseTo and CloseOpposite shapes when the host's
terms name one — the same shapes a HostSized{Open} may name. The units are
the sized opening on the declared side: a ReverseTo closes the opposite
position and opens them, a CloseOpposite closes that many units of the
opposite book (the whole-book Flatten path when it consumes all of it) and
opens nothing. Both still require an opposite book
(MatchRejectReason::NoOppositeExposure), and a CloseOpposite claim larger
than that book is InvalidTerms.
SizePrice chooses which price the basis converts at. Resolved (the
default) keeps today's price — the candidate's default resolved price for
AtMatch, the raw acceptance-point price for AtAcceptance. Signal sizes
against the expected market fill instead: the decision-point price at
placement (the current bar's close, or the last print, at submit), moved by
the run's slippage on the request's own side
(price ± slippage_ticks * price_tick) and then rounded onto the run's price
grid when NativeRunSpec::price_grid is set (HalfUp is the nearest tick;
NativePriceGrid::None leaves it unrounded).
The signal price is frozen when the request is accepted. It therefore composes
with SizeTime: with AtAcceptance both the price and the units are decided
at placement, and with AtMatch the basis still converts at that frozen signal
price at every later candidate, however far the market has moved. A Signal
request accepted with no decision point has no price to freeze and stays
TermsUnresolved.
SignalOnTick is the same rule measured on the instrument's own tick ladder
(NativeRunSpec::price_tick) instead of the run's fill grid, and quantized
before the slippage as well as after: round_tick(round_tick(price) ± slippage_ticks · price_tick).
Two things make it distinct from Signal, and both are generic. First,
price_tick is the instrument's resolution and the run already declares it as
the slippage multiplier, so a price the market prints lives on that ladder
whether or not the run also quantizes the prices it books — price_grid
governs fills, not what a signal was worth, and a run that leaves it at None
can still size against the printed price. Second, slippage is a whole number of
ticks, so it carries a ladder price to another ladder price: pre-rounding makes
round(p) ± n·tick exact and leaves the second rounding a binary64
re-normalization, where rounding only afterwards composes two different
quantizations of the same print.
The rule is generic — "size against the price you expect to pay" — and names no
source language. A source layer that divides by
nearest_tick(nearest_tick(signal_close) ± slippage · mintick) while booking
its own fill prices spells that as {SizePrice::SignalOnTick, price_tick = its mintick, slippage_ticks = its slippage}; one that has adopted
the kernel's fill grid spells the single-rounding form as
{SizePrice::Signal, price_grid = QuantizeFills, grid_rounding = HalfUp}.
The Pine adapter lowers its declaration-level default quantity
(default_qty_type cash and percent-of-equity, on a market entry whose
quantity is frozen at the command) onto Sized{CashValue, AtAcceptance, SignalOnTick, ExplicitUnits} and keeps exactly three things of its own, all of
them rounding or admission quirks the kernel deliberately does not model:
which equity a percentage is taken of and its ten-significant-digit money
rounding, which it hands over as the CashValue basis; its two lot floors,
which is why it asks for ExplicitUnits and floors the kernel's quotient in
its resolve_execution_terms override; and its own placement-time money-band
and affordability gates, which consume a quantity before any request exists and
so cannot be a kernel decision. The percentage fee reserve is the kernel's
(reserve_percent_fee). The generic conversion is available through
native_sized_units(). Source paths that keep HostSized{Open} retain their
source-specific floor in resolve_execution_terms, while the cash or equity
quotient comes from the kernel's sizing query. The two
lot floors stay the adapter's because neither is the kernel's SnapToGrid on
every input — the cash floor keeps a quotient a millionth of a lot under a
boundary raw, and the percent floor has no on-grid tolerance, so an exact lot
multiple such as 0.0392 on a 0.0001 grid comes out 0.0391 — both measured
in tests/test_adapter_sizing_relower.cpp ("the source lot floors are not
the kernel floor"). Paths whose
sizing price is not the signal rule — a fill-time resize, a pure-stop entry
sized at its trigger level, a typed percentage reversal sized from the
hypothetical flatten's balance — keep HostSized{Open} and their own terms
branch (same test, "pure-stop default entry keeps its own branch"), but that
branch converts through the same query: the at-fill default resize is
default_sizing_units() at the fill price, and a typed per-call quantity
(qty_type cash or percent-of-equity) is typed_quantity_units() — the kernel's
conversion under the source's grid floor — wherever it is read: at the fill, in
the placement admission, and in the pending-order fill-quantity probe
(strategy_pending_order_fill_qty), which answers the booked quantity at the
price it is asked about (tests/test_adapter_typed_entry_admission.cpp,
tests/test_adapter_fill_qty_probe.cpp). No cash / (price × point_value × fx)
quotient is restated in the adapter. Two computations stay its own. The
percentage exits: the kernel resolves a ScopeFraction as scope * fraction,
which is not scope * percent / 100, and no field reconciles the association.
And the money band's affordable price, sig10(sig10(equity) / notional per unit of price) compared against the sizing price at placement, at the terms
boundary and in the paired-reversal and carried-money checks: that is not a
conversion of money into units but TradingView's ten-significant-digit
whole-drop price (the famr / famr3 tapes of tests/test_tv_money_precision_l4b.cpp
and tests/test_tv_money_band_l4b.cpp), and on a decimal tie the kernel's
conversion and plain binary64 both refuse the lot the rule admits
(tests/test_adapter_fill_qty_probe.cpp, section 2; design row SZ10a).
NativeStrategyHost::native_sized_units(sized, price, equity, fx) is the
kernel's resolution as a pure query: units = cash / (price × point_value × fx)
with cash the basis value or fraction × equity, net of the percent fee
reserve when the intent asks for it, then the intent's grid policy — the very
function the kernel runs at acceptance and at the candidate. It moves nothing
and freezes nothing. nullopt means the run is not configured or the basis is
unresolvable at those inputs: a non-positive money or denominator, or a
below-one-step quotient under SnapToGrid. A host that gates a command on its
quantity before it submits — an affordability check, a sibling reservation —
reads the number here instead of keeping its own copy of the conversion
(tests/test_native_sizing_bases.cpp, "sizing preview").
An AtAcceptance quantity is also an admission input at placement, not only at
the candidate: when the request is accepted the kernel runs the run's own
opening admission — allowed_open_directions, max_abs_units,
max_open_lots and the initial-margin gate — against the frozen quantity at
the sizing price, converting at the acceptance point's rate, the one the
quantity was frozen at. A quantity the run cannot admit is
RequestRejectReason::PlacementAdmission at submit, so no request is ever
created for it. AtMatch has no placement quantity and keeps the candidate
gate it always had. A host that owns its own margin rule declares no kernel
margin, exactly as it does for the candidate gate, and only the caps apply.
ReductionSize gains ScopeFraction{fraction, claim, basis}, a fraction in
(0, 1] of the scope the reduce is bound to:
no::Request exit;
exit.intent = no::Reduce{no::ScopeFraction{0.5, no::ScopeClaim::NetOfSiblings}};
exit.owner = no::BindOpening{opening, cycle}; // half of that one lotThe bound scope is the whole book for an unbound or book-bound close, the one
opening for BindOpening, and the live selected roster for BindOpenings.
ScopeClaim::Gross takes the fraction of that scope as it stands.
ScopeClaim::NetOfSiblings first subtracts the units already claimed by the
live sibling reduces bound to the same scope, so a 50 % fraction beside a live
5-unit sibling on one 10-unit lot claims 5 gross and 2.5 net. Scope identity is
the request's own authority — opening handles, position cycles and cohort
handles — never a source identifier. A bracket child that is still waiting for
its parent has no scope and resolves only after the parent fill.
ScopeBasis chooses when the scope is measured. AtMatch (the default)
reads the bound scope as it stands at the matching candidate, so two 50 %
siblings executed in turn on one 10-unit lot close 5 then 2.5.
ScopeBasis::AtAcceptance freezes the scope's size when the request is
accepted — the placement-time live basis — so the same two siblings each keep
the ten they were placed against and close 5 + 5, and NetOfSiblings
subtracts live sibling claims from that frozen basis. The frozen basis sizes
the claim only; the live exposure still bounds what actually closes. A
replacement is a new command and re-freezes against its own acceptance. A
request accepted with no measurable scope never becomes resolvable.
The resolution is units = scope * fraction, one binary64 multiplication: a
caller that spells its size as a percent converts percent → fraction itself,
because scope * percent / 100 and scope * (percent / 100) are different
binary64 values. The result is floored onto quantity_grid like every other
engine quantity; a fraction that does not buy one whole step is
TermsUnresolved, and fraction outside (0, 1] is InvalidQuantityBasis at
submit. The one exception is a fraction whose product is its scope's held
total as it stands -- fraction == 1 of the gross scope, "close it all", for a
request that settles it in one fill (no point budget, no group deduction
pending against it that takes any): it resolves to that total, which is the
fold of the book's own lots and is not floored, so it closes the scope whole
whatever the grid (R5 lane K-ULP4; floored, it closed up to a step short, left
a dust lot, or found nothing to close). A pending total the units absorb takes
none, so such a request settles them all too (R5 lane K-OCA-KEEP; until then
the fraction was floored and left a dust lot, and a host-sized close answered
with its scope's total was refused InvalidTerms). A scope net of siblings'
claims, or frozen at acceptance at another total, is floored like any other
fraction.
The Pine adapter emits Sized for the default-quantity entries above and no
ScopeFraction (its percentage exits keep their own arithmetic);
native_order values belong to native_order_v7.
Pine/generated hosts derive from pineforge::source::PineStrategyHost, which
derives from NativeStrategyHost; handwritten native hosts also derive from
NativeStrategyHost. The source adapter/scheduler hash domain is
kSourceAdapterDomain pine_adapter.hpp:43
("pineforge-source-adapter/v4"), while the public C ABI remains version 4.
The ownership switch is complete: the compatibility loop and the source
pending-order type are gone, and source commands lower into native requests.
The installed-header check removes source/ and compat/pine/, then compiles
the declared native roots and native examples; its dependency files and nm
output are the evidence for this include boundary
(scripts/check_native_include_independence.py). The separately linkable
proof is PineForge::kernel — see Building the kernel only below.
The second runner module is a deliberately small example of those public seams. It contains no Pine command calls, formula, or protected engine write:
#include <pineforge/native_host.hpp>
#include <cstdint>
#include <optional>
#include <variant>
namespace {
namespace no = pineforge::native_order;
class NativeSelectedExample final : public pineforge::NativeStrategyHost {
std::optional<no::RequestHandle> opening_;
std::optional<no::RequestHandle> reversal_;
std::optional<std::int64_t> opening_cycle_;
int bars_ = 0;
bool child_submitted_ = false;
no::ExecutionTerms resolve_execution_terms(
const pineforge::NativeExecutionTermsFacts& facts) const override {
if (std::holds_alternative<no::RemainingDeferred>(facts.remaining)) {
return {facts.default_resolved_price, 2.0, no::OpeningShape::Transact};
}
return {facts.default_resolved_price, std::nullopt, no::OpeningShape::Transact};
}
pineforge::NativePrecommitVerdict validate_execution_precommit(
const pineforge::NativePrecommitView&) const override {
return pineforge::NativePrecommitVerdict::Proceed;
}
void on_native_bar(const pineforge::Bar&,
const pineforge::NativeDecisionContext&) override {
++bars_;
if (bars_ == 1) {
submit(no::Request{no::Transact{1.0}, "baseline-open", ""});
no::Request open;
open.intent = no::HostSized{no::HostSizedKind::Open, no::Side::Long};
open.label = "sized-limit";
open.trigger = no::Limit{101.0};
opening_ = submit(open).handle;
return;
}
if (!child_submitted_ && opening_ && opening_cycle_) {
no::Request child;
child.intent = no::Reduce{no::OwnerOpenedUnits{}};
child.label = "bound-stop";
child.trigger = no::Stop{99.0};
child.owner = no::BindOpening{*opening_, *opening_cycle_};
submit(child);
child_submitted_ = true;
}
if (bars_ == 3 && opening_ && opening_cycle_) {
no::Request selected{no::Flatten{}, "selected-flatten", ""};
selected.owner = no::BindOpenings{{*opening_}, *opening_cycle_};
const auto accepted = submit(selected);
if (accepted.handle) {
const pineforge::NativeCurrentExecution current{
*accepted.handle, pineforge::NativeCurrentPriceRule::NearestTick};
const auto preview = inspect_current_execution(current);
if (!preview.refusal &&
preview.settlement_readiness == pineforge::execution::Status::Applied) {
execute_current(current);
}
}
}
if (bars_ == 4 && !reversal_) {
no::Request reverse;
reverse.intent = no::ReverseTo{-1.0};
reverse.label = "exact-reverse";
reversal_ = submit(reverse).handle;
}
}
void on_native_applied(const no::ExecutionAppliedEvent& event,
const pineforge::NativeDecisionContext&) override {
if (opening_ && event.handle() == *opening_) opening_cycle_ = event.cycle_after;
}
};
} // namespaceThis is the shape of
examples/native/native_selected_strategy.cpp. The accompanying runner test
executes it once in batch and once through stream_begin / stream_push_bar /
stream_end; its selected close uses the current-point preview only as a
readiness observation before execute_current.
An immutable NativeFxCurve is staged only while the host is Ready, after
configure_native and before a batch run or a stream_begin. Its parallel
timestamp/rate arrays must have equal length, strictly increasing timestamps,
and finite positive rates; an empty curve clears the staged value.
account_fx remains the fallback before the first curve point.
pineforge::NativeFxCurve curve{{0, 900000}, {1.0, 1.01}};
const auto fx = host.configure_native_fx_curve(curve);
if (fx.status != pineforge::NativeSetupStatus::Applied) {
// Read fx.validation; engine storage was not changed.
}From C, strategy_configure_native_fx_curve_v1 answers -1 for every
refusal. strategy_configure_native_fx_curve_ext_v1 answers the same and, when
the kernel judged the curve, also writes pf_native_fx_curve_error_t — the C
twin of NativeFxCurveError: LENGTH_MISMATCH, NOT_STRICTLY_INCREASING,
NOT_FINITE_POSITIVE, ALLOCATION_FAILURE, WRONG_PHASE — and the offending
point's index (0 for a length mismatch). A refusal of the C layer itself (a
NULL argument, a bad handle, a wrong layout) leaves both untouched.
A curve declared through configure_native_fx_curve (C:
strategy_configure_native_fx_curve_v1) is the run's FX epoch: immutable
from Ready to the end of the run, refused with WrongPhase once the run is
Running, and named by the continuation identity before the first warmup bar. A
stream runs under it exactly as a batch does. The account converts at
account_currency_fx_at(effective time) of each driver point, confirmed input
reaches those points through the same consume_confirmed_input a batch uses,
and so a stream of confirmed bars converts — and takes its
NativeMarginCheckKind::FxRoll check points — exactly where a batch of the same
bars does, whether a step lands in the warmup replay or in realtime
(tests/test_native_margin_fx_roll.cpp, the stream twin: every offered check
point, requirement, receipt, closed row and the book left behind, ordinal for
ordinal). The last point's rate carries forward for as long as the stream
lives.
Two limits are permanent parts of that contract, not pending work:
- No realtime rate ingestion. The epoch is fixed at begin. A host that learns a new rate ends the stream and begins the next one under the new curve; the two runs have different continuation identities, which is the point — a rate that arrived mid-run would retroactively be a different run.
- Confirmed bars only. Tick-driven input is refused on a stream that
declares a curve (
"a declared native FX curve requires confirmed-bar stream input"; a refusal, not a failure — the stream keeps running). The reason is the conversion clock, not the curve: an observation hook runs before its print has moved the engine's presented clock, and a partially finalized slot calculates at its own OPEN, behind prints it has already matched. Re-measured with a step at T+90s: the T+100s and T+110s prints were observed with the presented clock still standing at T+70s, soactive_account_currency_fx()answered the pre-step rate there. That clock is what every SCRIPT-VISIBLE conversion reads —open_trade_profit(),marked_equity(), the equity curve — which is what the refusal guards, and it is a different surface from the margin model's own conversion: a check point converts at its cursor ("Margin and liquidation", above) and would be correct on the tick route too, but the money the script reads there would not be. Rather than convert one run on two clocks, the tick route stays closed. Moving that clock changes what the run's timestamp sinks (the stream state hash, trace rows) present on the tick route, so it is a change of its own, with its own pins, and not a condition this contract waits on.
The series of the mutable setter ingress (set_account_currency_fx_series,
the source providers' route) keeps its stream_begin refusal
("timestamped account-currency FX is not supported by streaming"): that
series belongs to an owner that revalues on a broker-open clock of its own,
which has no realtime route.
Both hosts on this page are built sources, not listings. They live under
examples/native/ with one standalone, Pine-free host per feature family the
kernel exposes. Each includes only <pineforge/native_host.hpp> (or the
toolkit / module header over it; the two C hosts <pineforge/pineforge.h>),
links PineForge::kernel, and prints its
summary line — the one carrying closed trades: — only after every check of
its own has passed:
| example | demonstrates | the suite that pins the same surface |
|---|---|---|
hello_kernel.cpp |
the smallest complete host: one market entry, one flatten | tests/test_native_example_batch.cpp |
hello_kernel_c.c |
the same host from C, through <pineforge/native_c_api.h> |
tests/test_native_c_api.c |
native_market_strategy.cpp |
batch and stream lifecycles; also the live runner's MODULE | tests/test_native_example_batch.cpp, tests/test_streaming.cpp |
native_selected_strategy.cpp |
HostSized, BindOpening(s), execute_current, ReverseTo; also a MODULE |
tests/test_native_example_selected.cpp, tests/test_native_current_execution.cpp |
native_bracket_strategy.cpp |
submit_bracket with three FromOwnerFill legs on a tick ladder — take-profit, stop-loss and a trail's arm threshold, each snapped Directional and asserted on the ladder (103.25, 98.00, 101.75); PendingUntilArmed, so the working rows go from 1 to 3 at the fill; the one-cancels-all group, whose take-profit fill cancels both siblings with CancelReason::Group |
tests/test_native_toolkit_bracket.cpp, tests/test_native_anchored_legs.cpp |
native_sized_report_strategy.cpp |
Sized{CashValue} / Sized{EquityFraction} resolved by the kernel; report_policy = KernelRecorded, report_open_position_at_end, reading fill_report |
tests/test_native_sizing_bases.cpp, tests/test_native_report_truth.cpp |
native_margin_strategy.cpp |
NativeMarginModel (initial gate, native_liquidation_price, kernel liquidation, MarginCallEvent); margin_check_allowed, resolve_margin_requirement |
tests/test_native_margin_model.cpp, tests/test_native_margin_hooks.cpp |
native_calc_on_fills_strategy.cpp |
NativeCalculationTrigger::BarCloseAndFills, on_native_recalculate, current_partial_bar, NativeOpenBarView::OpenOnly |
tests/test_native_calc_timing.cpp |
native_htf_strategy.cpp |
declare_timeframe_subscriptions, on_native_timeframe_bar, native_series_bar, gaps, a LazyComplete bucket |
tests/test_native_htf_subscriptions.cpp |
native_trail_risk_strategy.cpp |
Trail with TrailTicks, trail_state, native_working_requests, cancel_where; NativeRiskLimits::max_fills_per_day, native_risk_state, NativeRiskEvent |
tests/test_native_risk_limits.cpp, tests/test_native_trail_state_l5k.cpp |
native_price_grid_strategy.cpp |
NativePriceGrid None / QuantizeFills / QuantizeFillsAndTriggers and NativeGridRounding HalfUp / Directional on one sub-tick tape: raw against booked price per fill, a ladder level as a fixed point, the stop only the quantized path reaches, GridRequiresPriceTick |
the private test test_native_price_grid |
native_price_grid_c.c |
the same four runs from C: PF_NATIVE_SPEC_EXT_PRICE_GRID through strategy_configure_native_ext_v1, fills read back from strategy_native_events_v1 with the C++ host's numbers |
tests/test_native_c_api.c |
native_risk_limits_strategy.cpp |
the money limits: max_intraday_loss as a percent of the day's opening equity, max_consecutive_loss_days, max_drawdown; FlattenAndBlock and the kernel's own flatten (RequestOrigin::KernelRisk, ticket __kernel_risk__); CalendarDayInTimezone, the next-day re-arm, the ledger per day |
tests/test_native_risk_limits.cpp |
native_auxiliary_feed_strategy.cpp |
NativeRunSpec::auxiliary_feed: a "5" series over 15-minute inputs built from a 1-minute feed (NativeSeriesSource::AuxiliaryFeed), every bucket against its hand aggregation, routing by time, and the refusal without a feed (SubscriptionWithoutAuxiliaryFeed) |
tests/test_native_auxiliary_feed.cpp, tests/test_native_auxiliary_feed_stream.cpp |
native_open_lots_strategy.cpp |
native_open_lots(mark) lot by lot and native_marked_equity(mark); a FIFO partial reduce taking its share of the entry fee; the report accessors a host inherits; which report policy folds the extremes |
tests/test_native_open_lots.cpp |
native_fee_reserve_strategy.cpp |
Sized{CashValue} with reserve_percent_fee, from a JPY account on a USD-quoted instrument: 46 shares with the reserve and 47 without, both as native_sized_units and as fills; the commission of each leg (ExecutionAppliedEvent::current_ticket, NativeOpenLot::entry_commission, the closed row); the reserve is a no-op under CashPerExecution |
tests/test_native_sizing_bases.cpp |
native_fx_roll_strategy.cpp |
a NativeFxCurve staged with configure_native_fx_curve under a maintenance-only margin model: a restating point that is not a step, a 150 → 160 step offered as the NativeMarginCheckKind::FxRoll check point (its units, lot count, mark, time and the requirement at the new rate), native_liquidation_price moving with the rate, and the same run without the step, which liquidates nothing |
tests/test_native_fx_curve.cpp, tests/test_native_margin_fx_roll.cpp |
native_broker_hash_strategy.cpp |
the per-bar broker-state hashes (set_broker_state_hash_recording, KernelRecorded): one row per script bar in a batch and a stream; hash_host_extension, which moves every row and the scalar and no fill or continuation; replay and prefix closure within one driving; across drivings no shared row, the same closed rows and the same broker half (broker_state_hash_from_execution_hash) |
tests/test_native_report_truth.cpp, tests/test_native_host_hash_extension.cpp |
cmake -S . -B build -DPINEFORGE_BUILD_EXAMPLES=ON
cmake --build build -j
./build/examples/native/hello_kernel
ctest --test-dir build -R '^example_'PINEFORGE_BUILD_EXAMPLES (default OFF) builds each one as a standalone
executable and registers it as a CTest row that asserts two things: the host
exits 0, and it printed its summary line — closed trades: [1-9]; the market
host's line must also show a closed trade from its stream drive, the
selected host's pins its one host-sized opening of 2 units and its one
selected close, and the bracket, fee-reserve, FX-roll and broker-hash hosts'
lines pin their headline numbers: the three arm levels, the working rows and
the two group cancels; 46 against 47 shares and both commissions; the roll's
units and mark and the step-free run's zero margin calls; one hash row per
script bar, all of them moved by the extension, none shared between a batch
and a stream. CTest cannot assert both on one row (PASS_REGULAR_EXPRESSION
replaces the exit-code check), so every row runs its host through
examples/native/run_example.cmake, which fails on a nonzero exit, a signal, a
timeout or a missing line; an example registered without a summary line is a
configure error. The test_example_runner row
(scripts/test_example_runner.py) proves the runner fails in each of those
ways and that no example_* row sets a property that would override its
verdict. Each example is compiled with -UNDEBUG after the build-type flags,
as every test target is, so an assert() added to one aborts its row instead
of compiling to a no-op under Release's -DNDEBUG. The release and kernel
profiles of scripts/ci_verify.py turn the option on, so every example_*
row runs in the gate both with and without the source layer compiled (their
examples-assert-live stage refuses a configure in which an example's
compile command leaves NDEBUG defined);
scripts/check_native_include_independence.py compiles every source there
against the installed headers with the source trees removed, the two C hosts
with the C compiler. The market and selected examples are also built, from the
same sources, as the MODULE targets the live runner dlopens
(native_market_example, native_selected_example). Those two compiles take
-UNDEBUG too, and examples-assert-live checks them wherever the runner is
built: in the kernel profile beside the example_* executables, and in the
native profile, which builds no example_* target, for them alone. The
runner's own example module, native_live_example, built from
runner/examples/, also compiles with -UNDEBUG. It holds no assert(),
and the stage does not read it: it is not an examples/native source.
A host becomes such a module through one macro from
<pineforge/native_module.hpp>:
PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost);It defines strategy_create, strategy_free, strategy_set_input,
strategy_set_override, strategy_set_magnifier_volume_weighted,
run_backtest, run_backtest_full and report_free — and no other C symbol.
Every remaining runtime export (strategy_configure_native_v1, the
strategy_stream_* family, strategy_execution_contract,
strategy_get_last_error) is the engine's own; the generated strategy_create
references pf_abi_version() so a static link retains it. The host class must
derive from NativeStrategyHost and must not be final: the macro wraps it in
one derived class so the C boundary can write the presentation error string.
A host that is not written in C++ does not subclass NativeStrategyHost: it
hands the runtime a callback table and gets the same kernel back.
<pineforge/native_c_api.h> (included by pineforge.h) is that surface —
43 additive PF_API symbols implemented in src/native_c_host.cpp by
CCallbackHost, a final NativeStrategyHost that forwards each existing
virtual to the table. No new virtual, no epoch bump, and nothing about the
established C ABI moves: the compiled-strategy runtime symbols and their
counts are untouched, and scripts/check_c_abi_runtime.py pins the new set as
a second, disjoint inventory.
#include <pineforge/pineforge.h>
static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) {
struct host_state* state = user;
pf_native_request_v1 request = {0};
request.struct_size = (uint32_t)sizeof(request);
request.version = PF_NATIVE_API_VERSION;
request.intent = PF_NATIVE_INTENT_TRANSACT; /* or SIZED, REDUCE, REVERSE_TO, FLATTEN */
request.intent_value = 1.0;
request.label = "hello-long";
return strategy_native_submit_v1(state->handle, &request, NULL, NULL) < 0 ? 0 : 0;
}The complete host is examples/native/hello_kernel_c.c, the C twin of
hello_kernel.cpp; it is built and registered as a CTest row under
-DPINEFORGE_BUILD_EXAMPLES=ON and prints the same closed trades: 1.
Lifecycle. strategy_native_host_create_v1 returns an ordinary
pf_strategy_t: strategy_configure_native_v1, the whole strategy_stream_*
family, strategy_get_last_error and the read-only accessors all take it
unchanged, which is why streaming needs no new symbol. Release it with
strategy_native_host_free — not strategy_free, which does not own this
allocation. A batch run is strategy_native_run_v1; its report is released
with strategy_native_report_free_v1, because the unprefixed report_free is
one of the per-strategy exports the transpiler emits and is absent from
a runtime a C host links on its own.
A run's report. A report handed out belongs to the call that asked for
it, or it is empty, and a refused call never answers OK. The engine keeps the
rows of the last run it began, so BacktestEngine::fill_report — the one reader
behind strategy_native_run_v1, the native module wrappers and the
transpiler's run_backtest / run_backtest_full (run() then fill_report)
— publishes them only while the latest run or stream_begin call began
(reached reset_run_state). A call that did not begin (a begin outside
Ready, a refused bar array or run option, a calendar or timezone refusal, a
begin over a live stream) gets the empty report (every array NULL, every
count 0), overwritten and never freed. What such a call does to the handle
depends on why it was refused, and none of it is new. A begin on a handle that
is Completed, Failed or not configured, and a refused bar array or run
option, leave the lifecycle, the run identity and the retained rows as they
were. A calendar or timezone failure inside begin_ready (the begin proper)
comes after the consumed run-number high-water moved, and it 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 (Contract, Begin; a generated source host,
whose preparation calls configure first, latches Contract, Configure),
so the stream cannot be driven on after it, although its snapshot stays
readable. A repeated stream_begin on a stream that is already realtime is the
exception: it is refused (stream is already realtime) before the begin is
admitted, latches nothing, and leaves the stream Running. A run that began
owns its rows whatever became of it: a Completed report and a
started-then-failed run's partial rows are published unchanged.
strategy_native_run_v1 answers PF_NATIVE_OK only for a run that call
carried to Completed (the consumed run-number high-water moved) and
PF_NATIVE_E_RUN_FAILED otherwise; the reason stays in
strategy_get_last_error, strategy_get_last_error_code and
strategy_last_run_status (1). A Completed handle runs again after a
successful strategy_configure_native_v1. strategy_stream_fill_report is the
explicit snapshot: the same function asked for the rows' own owner, so a live
stream's rows survive a batch call refused over it, which reads empty, and the
batch reader stays empty after the snapshot until the next run call. A run call
made from inside a running run's callback reads empty the same way, while the
outer run, which began, publishes its own rows when it ends.
Commands. strategy_native_submit_v1, _replace_v1 / _replace_ext_v1,
_cancel_v1, _cancel_all_v1, _cancel_where_v1 and _execute_current_v1
follow the kernel's existing legality rule: inside a callback, or between realtime inputs. A command issued
anywhere else answers PF_NATIVE_E_STATE and changes nothing — the kernel
throws there, and the C boundary contains that throw rather than letting it
unwind through the C frame. A rejected command names its
RequestRejectReason in the reject out-parameter:
strategy_native_submit_v1 has always had one, and
strategy_native_replace_ext_v1 is _replace_v1 with the same parameter — a
second symbol rather than a wider signature, because this header's signatures
never change within a major version (the reason
strategy_configure_native_ext_v1 exists beside strategy_configure_native_v1).
_cancel_v1 needs no such spelling: its status is its reason.
strategy_native_position_v1,
_working_len_v1 / _working_get_v1 (the working view, copied out),
_open_lot_count_v1(s, mark) / _open_lot_get_v1 (the open-lot snapshot,
copied out into pf_native_open_lot_v1 — the C spelling of
native_open_lots(mark), with the two strings borrowed until the next count
call, exactly like the working rows) and _events_v1 read the run back; _state_v1 reads the lifecycle and its typed
failure. _cancel_where_v1 takes the text and a
pf_native_request_field_e (PF_NATIVE_FIELD_COMMENT / _LABEL): an
unknown selector is PF_NATIVE_E_TAG and a NULL text is
PF_NATIVE_E_ARGUMENT, because "" is the text every request carrying no
such field matches.
Reading the kernel back. Eight accessors answer what the C++ host reads
off itself. Four of them are std::optional in C++, and PF_NATIVE_ABSENT
(1, a non-negative outcome, not an error) is the C spelling of that empty:
strategy_native_partial_bar_v1 (current_partial_bar() — the bar so far
through the last path point consumed, absent in the bar's own close calculation, where the callback
already holds the complete bar), _series_bar_v1 (native_series_bar() — the
latest completed bucket of a declared subscription, absent before its first
delivery and on every bar a PF_NATIVE_GAPS_CLEAR series publishes nothing on),
_trail_state_v1 (trail_state() — activation, running best, current level,
absent when the handle is not a live trail) and _liquidation_price_v1
(native_liquidation_price(), which also writes NaN when it is absent). The
other four always answer: _risk_state_v1 (the risk ledger, all zeros for a run
that declares no risk block), _marked_equity_v1, _recalculations_v1 (the
driven and suppressed counts — the script bar's own close calculation is not
a recalculation, so a BarClose run drives zero) and _continuation_hash_v1.
strategy_native_cancel_where_v1 is cancel_where(): it matches the text
the request carries in the field the third argument names — its comment,
what a group of requests can share, or its label, one request's own
identity — and answers how many left the book. "" is the text a request
carrying no such field matches, so it takes the comment-less requests rather
than all of them; NULL is PF_NATIVE_E_ARGUMENT.
strategy_native_declare_subscriptions_v1 is
declare_timeframe_subscriptions(): called from inside on_run_begin, it
replaces the subscriptions the extension staged, and anywhere else — or for
a list this run's input timeframe would refuse — it stages nothing and
answers PF_NATIVE_E_STATE.
The request. pf_native_request_v1 is translated field by field into
native_order::Request and is never cast onto it. It carries the intent
(including Sized, with basis, side, time and grid policy), the trigger
(Market / Limit with fill_through / Stop / StopLimit / Trail, with
the tick spellings, the arm price and the seeded start of the running best),
the FromOwnerFill anchor, the
capacity, the owner relation with its incarnations and cycle, the group and
its effect, and the label and comment. The two anchored-leg knobs ride an
additive tail behind PF_NATIVE_REQUEST_V1_BASE_SIZE (anchor_rounding,
visibility), and later tails add the sizing detail, the trail seed and the
arm relation (arm_first_match, arm_scope): the runtime accepts every
published length, so a caller compiled against an earlier layout keeps working
and gets the defaults. Where an anchored leg is armed is answered through
pf_native_callbacks_v1::on_anchored_level, the C route of
resolve_anchored_level, or left to the kernel's level.
PF_NATIVE_INTENT_HOST_SIZED is refused with PF_NATIVE_E_UNSUPPORTED but for
the two closes the kernel spells no other way — a cohort close
(PF_NATIVE_OWNER_BIND_COHORT) and a WAIT_FOR_APPLIED child that carries
the arm tail — whose units on_close_units answers: HostSized is otherwise
the adapter's sizing seam, and a C host sizes with Sized.
The run specification. strategy_configure_native_v1 still takes the v1
spec. The fields added after it — report policy and the open-position row,
the price grid and its rounding, calculation timing and its recalculation
bound, the open-bar view, the generic margin model, higher-timeframe
subscriptions, the generic risk limits, and the later tails below — travel in
pf_native_run_spec_ext_v1, passed together with the base spec to
strategy_configure_native_ext_v1. It replaces
strategy_configure_native_v1 rather than following it, because the kernel
configures each run exactly once: a second extended configure of a Ready
handle is refused without changing it. It does not carry identity, which the
base spec owns, or timeframe_undetected, which no C spec can set: a C run
always names both timeframes.
Every enum-valued word of the two specs is a uint32_t word holding a
value of a C enumeration that names its kernel enumeration's values one for
one, each integer pinned by a static_assert in src/native_c_host.cpp.
The base spec's three and seven of the extension's are below; the
extension's other eleven — margin_check and the risk and intrabar / policy
tails' words (intrabar_kind names a variant's alternatives) — carry their
C enumeration beside the field in native_c_api.h:
| word | spec | C enumeration | values, the default first |
|---|---|---|---|
fee_kind |
base | pf_native_fee_kind_e |
PF_NATIVE_FEE_PERCENT, _CASH_PER_UNIT, _CASH_PER_EXECUTION |
close_execution |
base | pf_native_close_execution_e |
PF_NATIVE_CLOSE_EXECUTION_NEXT_ELIGIBLE_POINT, _AFTER_CALCULATION |
allowed_open_directions |
base | pf_native_open_directions_e |
PF_NATIVE_OPEN_DIRECTIONS_BOTH; also _NONE (the zero word: a zero-filled spec admits no opening), _LONG, _SHORT |
report_policy |
ext | pf_native_report_policy_e |
PF_NATIVE_REPORT_HOST_RECORDED, _KERNEL_RECORDED |
price_grid |
ext | pf_native_price_grid_e |
PF_NATIVE_PRICE_GRID_NONE, _QUANTIZE_FILLS, _QUANTIZE_FILLS_AND_TRIGGERS |
grid_rounding |
ext | pf_native_grid_rounding_e |
PF_NATIVE_GRID_ROUNDING_HALF_UP, _DIRECTIONAL |
calculation |
ext | pf_native_calc_trigger_e |
PF_NATIVE_CALC_TRIGGER_BAR_CLOSE, _BAR_CLOSE_AND_FILLS, _EVERY_MODELED_POINT |
open_bar_view |
ext | pf_native_open_bar_view_e |
PF_NATIVE_OPEN_BAR_VIEW_COMPLETE, _OPEN_ONLY |
margin_sizing |
ext | pf_native_liquidation_sizing_e |
PF_NATIVE_LIQUIDATION_SIZING_RESTORE_MINIMUM, _SHORTFALL_MULTIPLE, _FLATTEN |
event_retention |
ext | pf_native_event_retention_e |
PF_NATIVE_EVENT_RETENTION_WINDOW, _FULL, _COMMANDS; a caller that does not send the word keeps _FULL |
A value outside its enumeration is PF_NATIVE_E_TAG from
strategy_configure_native_ext_v1, and the handle stays unconfigured.
NativeReportPolicy::KernelRecordedAtHostMarks has no C value: its host
marks each report point from inside its own callbacks, and the callback table
has no call that marks one, so its integer, 2, is refused like any other.
strategy_configure_native_v1 has no boundary check of its own for the three
base-spec words: the kernel's validation judges them, and a bad one answers
-1 and leaves the host Failed with PF_NATIVE_FAILURE_INVALID_SPECIFICATION.
The kernel still fails a Ready handle under its Contract rule. The C boundary
refuses a Running handle without changing its state, including when a callback
calls it. It also refuses a Completed host's
on_hash_extension callback during a read-only hash query. A Completed handle or one Failed by a
cooperative abort may be configured for another run; the kernel judges reuse
constraints and can fail a refused reuse with PF_NATIVE_FAILURE_CONTRACT. An
Aborted handle is reusable with the same session key and a higher run number; a
handle whose configure call failed remains Failed and cannot be configured again.
The call has no typed out-parameters.
strategy_configure_native_ext_result_v1 is the typed spelling of
strategy_configure_native_ext_v1: the same inputs plus two out-parameters
that receive the kernel's pf_native_spec_error_t / pf_native_spec_field_t
pair. With both NULL it is exactly strategy_configure_native_ext_v1. With
either set it also configures the next run of a Completed handle, or of one
Failed by a cooperative abort, as strategy_configure_native_v1 reuses a
host, and it names a refused reuse: a changed session key
(PF_NATIVE_SPEC_ERROR_SESSION_KEY_CHANGED_ON_REUSE, field SESSION_KEY) or a
run number not above the highest the handle has run
(PF_NATIVE_SPEC_ERROR_RUN_NUMBER_NOT_ABOVE_CONSUMED_HIGH_WATER, field
RUN_NUMBER), each PF_NATIVE_E_ARGUMENT. That refusal latches a Completed
handle Failed with PF_NATIVE_FAILURE_CONTRACT; an aborted handle keeps its
abort and may be configured again. A Ready or Running handle, and one Failed
by anything but an abort, answer PF_NATIVE_E_STATE with
PF_NATIVE_SPEC_ERROR_WRONG_PHASE and nothing changes; success writes NONE
/ NONE and leaves the handle Ready. The third word appended with them,
PF_NATIVE_SPEC_ERROR_ABORTED_HOST_NO_REUSABLE_RUN_SPEC, is defensive: a
cooperative abort latches only while a run holds its spec, and the Failed
state keeps it, so no public call reaches it.
pf_native_run_spec_ext_v1's third of six published lengths,
PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE, ends the tail after the base layout
(PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE) and the risk tail
(PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE); the runtime accepts it and the other
five. That tail appends what the header used to list as unrepresentable — the retained intrabar path
(PF_NATIVE_SPEC_EXT_INTRABAR), the four feed-shape and presentation
policies (PF_NATIVE_SPEC_EXT_FEED_POLICY: slot labels, feed tolerance, the
forced path order, abort reporting), and the margin model's remaining knobs
(equity basis, level base, the liquidation ticket strings, and
NativeLiquidationCheck::PathAdverseExtremeMark, which had no C value at
all). A caller sending an earlier length cannot set a mask bit its struct has
no fields for: that is PF_NATIVE_E_STRUCT, exactly as the risk bit already
was.
The intrabar block is also what makes on_sub_bar reachable: only
PF_NATIVE_INTRABAR_LOWER_TF retains a finer feed, and only a retained feed
has sub-bars of its own. A PF_NATIVE_SLOT_LABEL_FEED_TOLERANT run keeps the
caller's own labels and delivers none.
pf_native_request_v1 likewise has five published lengths now — base, plus
the anchored-leg tail (PF_NATIVE_REQUEST_V1_ANCHOR_SIZE), plus the
sizing detail (PF_NATIVE_REQUEST_V1_SIZING_SIZE): size_price (SizePrice:
RESOLVED, SIGNAL, SIGNAL_ON_TICK) and reduce_basis (ScopeBasis:
AT_MATCH, AT_ACCEPTANCE), plus the trail seed
(PF_NATIVE_REQUEST_V1_SEED_SIZE): trail_best_seed and
trail_has_best_seed (Trail::best_seed, where the running best starts),
and the current arm-relation tail.
pf_native_working_v1 is the first readout with additive tails, and has
three published lengths: the base (PF_NATIVE_WORKING_V1_BASE_SIZE), that plus
trail_has_arm_price (PF_NATIVE_WORKING_V1_ARM_SIZE), and the current one,
which appends the anchor and the owner relation. trail_has_arm_price is the
request's own flag read back, so a trail with no arm price and one armed at
0.0 — both of which read p2 = 0 — are different rows. An anchored trail
reads back the spelling it was submitted with until its owner fills and the
arm installs the level. A readout owes its caller one thing an input does
not: the runtime writes it, so a caller sending an earlier length is filled
exactly that far and never past it, and any other length is
PF_NATIVE_E_STRUCT.
The risk block (PF_NATIVE_SPEC_EXT_RISK) was the first additive tail in
this header. It appends risk_* fields — the two loss limits as a value plus
a percent flag, the two counts, the day basis and the breach action, each
limit opt-in through its own has_ flag — past reserved0, which gave
pf_native_run_spec_ext_v1 its second published length — beside
PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE (the layout this header first shipped,
defined as the offset of the first appended field rather than as a literal, so
it stays right on every target) — ending at PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE.
Each subscription row (pf_native_subscription_v1) carries lookahead and
gaps as uint32_t words holding a pf_native_lookahead_e
(PF_NATIVE_LOOKAHEAD_AT_COMPLETION / _AT_FIRST_INPUT) and a
pf_native_gaps_e (PF_NATIVE_GAPS_HOLD / _CLEAR) value, and may repeat a
tf: the rows are series instances,
delivered under their own index, and only their authoritative_bars are
shared. gaps occupies the word the row published as reserved0 — a
reserved word every layout required to be zero — so the row's size and field
offsets are unchanged, a caller that zero-fills it keeps PF_NATIVE_GAPS_HOLD
(barmerge.gaps_off), and a value outside either enumeration is
PF_NATIVE_E_TAG.
A caller sending the base length keeps working unchanged and is refused with
PF_NATIVE_E_STRUCT if it sets the risk bit it has no fields for.
tests/test_native_c_api_frozen_header.cpp
configures a host from the frozen v1 copy of the struct, so that acceptance is
executed rather than asserted.
The auxiliary finer feed (PF_NATIVE_SPEC_EXT_AUXILIARY_FEED) is the third
additive tail, behind the risk one and the intrabar / policy one:
auxiliary_tf, auxiliary_bars, auxiliary_n, a reserved word that must be
zero, and subscription_sources — an optional array of
pf_native_series_source_e, one word per subscription row, NULL meaning
every series is built from the input (the subscription row itself has no spare
word left, so the source rides beside it rather than in it). The event
retention (PF_NATIVE_SPEC_EXT_EVENT_RETENTION) is the fourth:
event_retention, a pf_native_event_retention_e word, and a reserved word
that must be zero. The quantity tolerance (PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE,
R5 lane K-ULP4) is the last: one double, quantity_tolerance. The struct
therefore has six published lengths and the runtime accepts each:
PF_NATIVE_RUN_SPEC_EXT_V1_BASE_SIZE, PF_NATIVE_RUN_SPEC_EXT_V1_RISK_SIZE,
PF_NATIVE_RUN_SPEC_EXT_V1_POLICY_SIZE, PF_NATIVE_RUN_SPEC_EXT_V1_AUXILIARY_SIZE,
PF_NATIVE_RUN_SPEC_EXT_V1_RETENTION_SIZE (each the offset of the first field
of the tail behind it, not a literal) and the current sizeof; a caller is
refused with PF_NATIVE_E_STRUCT for a bit whose tail it does not carry, and
any other length is refused outright. A caller that does not set the
retention bit keeps PF_NATIVE_EVENT_RETENTION_FULL, so every caller
compiled before the tail existed reads back the record it always did; one
that does not set the tolerance bit keeps the exact settlement. A realtime stream appends later feed
bars with strategy_native_append_auxiliary_bars_v1, the C spelling of
append_auxiliary_bars: PF_NATIVE_OK, or PF_NATIVE_E_STATE for every
by-name refusal above with the reason in strategy_get_last_error.
tests/test_native_c_api.c runs the batch, the stream and each refusal from
pure C.
The callback table. pf_native_callbacks_v1 has four published
lengths and the runtime accepts each: the layout this header first shipped
(PF_NATIVE_CALLBACKS_V1_BASE_SIZE, again the offset of the first appended
field rather than a literal), that plus seven hooks — the six below and
on_close_units (PF_NATIVE_CALLBACKS_V1_HOOKS_SIZE), that plus the policy-hook tail
(PF_NATIVE_CALLBACKS_V1_POLICY_SIZE), and the current one, whose trailing
reserved1 marker (required zero, checked at host creation) says the host reads the
lot-excursion facts' entry_commission tail. A host compiled against an
earlier layout keeps working and simply has none of the later hooks
installed, which is exactly the kernel's own default for each. The policy-hook
layout already receives the decision's session tail; the later marker affects
only the lot-excursion facts tail.
Two of the six are ordinary observation callbacks. on_recalculate is
on_native_recalculate: every calculation of the run arrives there, tagged
with a pf_native_calc_reason_e and, for an ORDER_FILL, the applied
execution that caused it. Installing it replaces on_bar for every
calculation, exactly as overriding on_native_recalculate replaces the C++
default forwarding; leaving it NULL keeps the established contract where the
kernel forwards every calculation to on_bar. on_sub_bar is
on_native_sub_bar, delivered once per completed lower-timeframe sub-bar of
a retained lower feed.
The other four are answering hooks — on_margin_requirement
(resolve_margin_requirement), on_margin_check (margin_check_allowed),
on_margin_call_units (resolve_margin_call_units) and on_lot_excursion
(owns_lot_excursions + closed_lot_excursion) — and they read their return
value differently: it is a pf_native_answer_e selecting WHOSE answer the
kernel uses, not a success code. Every value is therefore in contract and an
answering hook can never fail the run. That split is not cosmetic: those four
are consulted from kernel paths that are not inside the consumer's
callback guard, so a failure raised there could not be latched without
unwinding through them, which is the one thing this boundary must never do. A
host that must abort does it from an observation callback, where the
established "non-zero ends the run Failed" rule is untouched.
A fifth answering hook, on_close_units, is the units half of
resolve_execution_terms; the price and shape half is on_execution_terms. It is
consulted for PF_NATIVE_INTENT_HOST_SIZED candidates and nothing else, and
it is what makes PF_NATIVE_OWNER_BIND_COHORT reachable: the kernel pairs
that owner with exactly one intent, a host-sized close, whose quantity comes
from the terms pass. pf_native_close_view_v1 carries scope_exposure_units
— for a cohort close, the live units of the roster's own openings — plus the
price the kernel would settle at and the cursor; the price and opening-shape
halves of the terms answer stay the kernel's.
So a C cohort close is: strategy_native_cohort_open_v1, one
strategy_native_cohort_add_v1 per opening (and _remove_v1 to take one back
off), then a PF_NATIVE_INTENT_HOST_SIZED request with
owner = PF_NATIVE_OWNER_BIND_COHORT and cohort set, with on_close_units
answering how much of the roster's exposure to take. HOST_SIZED under any
other owner, but a WAIT_FOR_APPLIED child carrying the arm tail (above),
stays PF_NATIVE_E_UNSUPPORTED: every other host-sized shape
needs the parts of the terms answer that are not exposed, so it is refused at
submit rather than accepted and then left unresolvable at the candidate.
Installing on_lot_excursion at all is owns_lot_excursions() == true: the
consumer then stops sampling excursion at matched trigger
prices for the whole run and every closing row takes both magnitudes from the
hook — so a lot the hook declines gets the kernel's own zero magnitudes,
because nothing was sampled for it. The facts it is handed,
pf_native_lot_excursion_v1, end in an additive tail: entry_commission, this
closing slice's share of the entry fee in account currency (the C++
ClosedLotExcursionFacts::entry_commission). Only a table sent at the current
length receives it; one sent at an earlier length is handed the facts with
struct_size = PF_NATIVE_LOT_EXCURSION_V1_BASE_SIZE, which stops before it.
The three margin hooks share one view
POD, pf_native_margin_view_v1, whose fields are documented per hook and
zero where that hook has no such fact, exactly as pf_native_event_v1's
union is.
The owner of the excursions also says where each lot's opening fill sat on
its entry bar: strategy_native_declare_opened_lot_entry_bar_mask_v1 is the
C spelling of the protected BacktestEngine::declare_opened_lot_entry_bar_mask.
It takes the lot's entry incarnation
(pf_native_applied_v1::opened_lot_incarnation), the whole entry bar and a
pf_native_opened_lot_fill_point_e — ON_PATH for a fill at a price the
bar's path reaches, AFTER_PATH for one at the bar's closing point — and the
kernel derives which ends of the bar the path had reached before the fill,
walking the bar in the run's own leg order (the spec extension's
path_order, the order the matcher walks; the open-proximity rule under
AUTO), and hands both flags back as
pf_native_lot_excursion_v1::entry_bar_high_masked /
entry_bar_low_masked. It is legal inside on_applied alone and
PF_NATIVE_E_STATE everywhere else. Being a member of the base rather than
of NativeStrategyHost, it is a row of the COVERAGE block's second list,
BASE-CLASS SEAMS (below). The entry-bar mask scenario of
tests/test_native_c_api.c reproduces the C++ witness
tests/test_e6_entry_bar_mask_declaration.cpp number for number.
The 1.0 C boundary. A C host drives the same kernel a C++ host does, and
where the C surface declares a field or a call it answers the C++ value
(tests/test_native_c_api.c and its C++ twins). It does not declare
everything. Every C++ capability the 1.0 C surface does not expose is a row
below, with its reason, the C route where there is one, and the row that fails
when the gap closes or the capability goes. This C surface ships in
v1.0.0 (v0.13.1, the last 0.x release, has no native_c_api.h), and 1.0
makes no C/C++ parity claim beyond the declared fields and calls, and none
for these rows. Four gaps are
planned for 1.1.0: the execution preview, the applied event's origin and
label, a closed row's entry comment and the replace options. The rest are
not scheduled. The checker rows are three kinds: a [--] row
of the header's COVERAGE block (one line per public member of
NativeStrategyHost, with its C spelling or the reason it has none), a named
exclusion in ENUM_TWINS, and a C_V1_EXCLUSIONS row. The last two live in
scripts/check_native_c_api_surface.py. A C_V1_EXCLUSIONS row fails when its
C++ declaration goes, or when its C spelling appears in the record it names
(each pattern also catches a suffixed or prefixed spelling), and the checker
fails when this table cites a row once too few or too many.
| C++ capability absent from the 1.0 C surface | reason, and the C route | checker row |
|---|---|---|
prepare_native_begin |
Offered once per begin with that begin's arguments (NativeBeginArgs: the bars, the timeframe literals, the magnifier settings, the stream facts, and the rich overload's inputs, symbol and opaque overrides). A C host supplies each of them itself: it declares its run with strategy_configure_native_ext_v1 and begins in on_run_begin. |
COVERAGE [--] prepare_native_begin |
inspect_current_execution |
Scheduled for 1.1.0. The preview (NativeCurrentExecutionPreview) carries the refusal, the settlement readiness, the account projection, the closed rows' P&L and the terms outcome, and moves nothing. C has no non-mutating call: strategy_native_execute_current_v1 applies the command, so it is not a preview. The facts are representable (pf_native_precommit_view_v1 flattens them for on_precommit); the call is what is missing. |
COVERAGE [--] inspect_current_execution |
submit_market, replace_market |
C++ conveniences that refuse a non-market trigger, capacity, owner, group, anchor or late-sized intent instead of dropping it. C has no market-only guard: the same request is strategy_native_submit_v1 / _replace_v1 with PF_NATIVE_TRIGGER_MARKET and a zero-filled struct. |
COVERAGE [--] submit_market, [--] replace_market |
native_closed_rows_amended |
A C++ host that writes the kernel's closed rows names the first one it changed here. C has no write access to the rows, so it has nothing to name. | COVERAGE [--] native_closed_rows_amended |
native_aggregates_input_bars() |
Whether the configured run's script bars are buckets the kernel gathers from its input bars (native_calendar::pairing_aggregates of the pairing it resolved), since R5 lane H-THIN. Its one reader is the Pine host, which re-dates a closed row onto the aggregated script bar TradingView reports; a C host reads the rows the kernel books as they are, and names both timeframes in its own spec. |
COVERAGE [--] native_aggregates_input_bars |
the deprecated hash_source_extension |
Kept so an existing C++ subclass compiles; C has only ever had the current spelling, on_hash_extension. |
BASE-CLASS SEAMS [--] hash_source_extension |
report points at the host's own cadence (NativeReportPolicy::KernelRecordedAtHostMarks) |
Under it the host marks each report point from inside its own callbacks; a C++ host marks through NativeStrategyHost::mark_native_report_point, which returns false outside a running KernelRecordedAtHostMarks run. C has no call that marks one: pf_native_report_policy_e leaves value 2 unnamed and the C host refuses it (PF_NATIVE_E_TAG), because a C host could only declare a series nobody records. A C host's recorded series is PF_NATIVE_REPORT_KERNEL_RECORDED. |
COVERAGE [--] mark_native_report_point, ENUM_TWINS["pf_native_report_policy_e"], the KernelRecordedAtHostMarks exclusion |
ReplaceOptions (retain_trigger_state, keep_handle, keep_binding) |
Scheduled for 1.1.0. The C replace calls take no options word: a plain replace is available, and carrying the trigger state, the handle or a book binding across it needs C++. | C_V1_EXCLUSIONS["replace_options"] |
ExecutionAppliedEvent beyond the fill |
The event carries its definition (origin, label, comment, the request), the closed-row range it booked, the remaining units and allowance before and after, the execution scope and the whole cursor. pf_native_applied_v1 carries the fill's prices, units, ticket, cycles, terminal reason and incarnations only. Origin and label are scheduled for 1.1.0; the rest has no lane. Inside on_applied a C host cannot tell a kernel liquidation from its own fill (on_margin_call arrives after it), and it finds the rows the fill booked only by probing strategy_closed_trade_entry_id for the new row count. |
C_V1_EXCLUSIONS["applied_event_tail"] |
a closed row's entry comment (closed_trade(i).entry_comment) |
Scheduled for 1.1.0. No C accessor returns it and pf_trade_t carries no strings. strategy_closed_trade_entry_incarnation names the request whose fill opened the lot, whose comment the C host wrote itself; while the lot is open, pf_native_open_lot_v1::entry_comment reads it. |
C_V1_EXCLUSIONS["closed_entry_comment"] |
closed rows in place (closed_trade_count() / closed_trade(i), report_trade_count() / get_report_trade(i)) |
A C++ host reads a closed row whole, at any time, every Trade field included. A C host reads the numeric fields from a filled pf_report_t (the run's strategy_native_run_v1 output, or strategy_stream_fill_report), and the strings, the close cause and the entry incarnation by report index. |
C_V1_EXCLUSIONS["closed_rows_in_place"] |
the working request's whole definition (NativeWorkingRequest::definition) |
pf_native_working_v1 reads back the request's tags, levels, relation tail, origin, label and comment. It leaves out a reduce's size kind, claim and basis, the Sized block (side, basis, time, price, grid policy, fee reserve), Limit::fill_through, a host-sized request's kind and side, every BindOpenings handle past the first, the predecessor, root, priority and kept binding, and the trail's seed. The seed is a submission value (pf_native_request_v1::trail_best_seed); the live ride is strategy_native_trail_state_v1. |
C_V1_EXCLUSIONS["working_definition"] |
NativeDecisionContext::closes_session_day_open_ended |
The C decision carries the other three session-day facts (in_session, opens_session_day, closes_session_day). The fourth differs from closes_session_day only on a batch's final bar, for a host recomputing a batch whose last input is still forming; a C host's live edge is a strategy_stream_* stream, and the decision's session tail gives the calendar facts for a host that applies its own rule. |
C_V1_EXCLUSIONS["open_ended_session_day"] |
| the rest of the callback contexts | The decision has no input interval, no source price time, no driver statistics and no quote origin ordinal (NativeCurrentPointView::quote_origin_ordinal). on_input gets the bar, its index and whether it completes a script interval, without the input or script interval; on_tick gets no sequence. |
C_V1_EXCLUSIONS["callback_contexts"] |
host-sized openings, and host-sized closes outside two shapes (HostSized{Open}, HostSized{Close} under Independent, BindOpening or BindOpenings) |
The C surface accepts PF_NATIVE_INTENT_HOST_SIZED as a cohort close or a Book-scoped WAIT_FOR_APPLIED child only, and refuses every other use as PF_NATIVE_E_UNSUPPORTED. A C host sizes an opening with Sized. |
C_V1_EXCLUSIONS["host_sized_opening"]; the refusal is asserted in tests/test_native_c_api.c |
the units answer of resolve_execution_terms |
A C++ hook may replace the kernel's units for an unresolved Sized or ScopeFraction candidate. on_execution_terms answers the price, the opening shape and the grid policy only, and on_close_units is consulted for a host-sized candidate and nothing else. |
C_V1_EXCLUSIONS["terms_units"] |
ClosedLotExcursionFacts::account_fx |
The rate a closing row's P&L and excursion columns take, so an excursion owner answering in the row's currency converts at it. pf_native_lot_excursion_v1 has no rate, so a C owner in an FX-converted run cannot. |
C_V1_EXCLUSIONS["excursion_fx"] |
folding any bytes into the broker-state hash (hash_host_extension's BrokerStateHashSink) |
A C++ override folds what it likes and may replace the default. on_hash_extension answers one 64-bit digest, which the kernel folds after its own bytes under a tag, so a C host's extended hash never equals a C++ host's own fold of the same state. |
C_V1_EXCLUSIONS["hash_sink"] |
declare_opened_lot_entry_bar_mask outside on_applied |
The C++ seam has no phase rule; the C spelling is legal inside on_applied alone and PF_NATIVE_E_STATE everywhere else. |
C_V1_EXCLUSIONS["entry_bar_mask_phase"]; the refusals before and after a run are asserted in tests/test_native_c_api.c |
NativeRunSpec::timeframe_undetected |
No C spec carries it and the base spec requires both timeframe literals, so a C run always names its timeframes. | C_V1_EXCLUSIONS["timeframe_undetected"] |
instrument feeds (NativeRunSpec::instrument_feeds, NativeSeriesSource::InstrumentFeed) |
No lane. A C run spec installs no instrument feed and a C subscription row carries no instrument key, so a C host cannot read another instrument's bars through the kernel: pf_native_series_source_e leaves InstrumentFeed unnamed, and pf_native_spec_error_e / pf_native_spec_field_e name its refusals only so the translation stays exhaustive. strategy_set_symbol_feed and its three siblings in pineforge.h are the Pine source host's door (a bare host answers them -1), not a C host's. |
C_V1_EXCLUSIONS["instrument_feeds"] |
run statistics through the inherited accessors (net_profit(), open_profit(), open_trades_capital_held(), max_runup_percent(), max_drawdown_percent(), active_account_currency_fx(), max_contracts_held_all() and its long / short siblings, eventrades()) |
Live, C reads strategy_current_equity, strategy_position_size and strategy_native_marked_equity_v1. Everything else comes from a filled report's pf_metrics_t, whose drawdown and run-up are walked from the recorded curve; C has no maximum contracts held and no capital held. |
C_V1_EXCLUSIONS["inherited_statistics"] |
the hook views' definitions (NativeExecutionTermsFacts, NativePrecommitView) and the cursors |
The C++ views carry the request's definition, its allowance or scope, and the settlement readiness; the C views carry the intent and trigger tags. A C cursor carries the ordinal, the effective time, t, the interval index, the provenance and the phase, never the coordinate's interval instants or source price time, and event rows and the decision drop t. |
C_V1_EXCLUSIONS["hook_views"] |
| the command events' payloads | pf_native_event_v1 flattens the event kinds into one tagged record with each kind's documented fields. It drops the definitions and the rejected or attempted requests, the command surface, the authority before and after, the remaining, allowance and pending projections, the cause and receipt ids, the attempted terms and the terms' resolved input, the requested delta, prior adjustment ids and ReplacedEvent::kept_handle(). |
C_V1_EXCLUSIONS["event_payloads"] |
the command results (SubmitResult, ReplaceResult, CancelResult, NativeCurrentExecutionResult) |
C returns the handle or the refusal, or a status, without the event ordinal; strategy_native_execute_current_v1 returns the outcome's code, not the event. The event history (strategy_native_events_v1) is the route. |
C_V1_EXCLUSIONS["command_results"] |
native_state()'s staged spec and failure context (NativeStateView::spec, NativeFailure::context) |
pf_native_state_v1 carries the lifecycle, the failure's code, operation, discriminator and ordinal, and the phase facts, but not the spec or the failure's cause ordinal, recipient and cursor. |
C_V1_EXCLUSIONS["state_view"] |
two minor ones: trace(name, value), and re-declaring the optional hooks after creation |
A C++ host writes report trace rows; C can turn tracing on (strategy_set_trace_enabled) and read the rows a report carries, not write one. A C++ declaration of on_bar_open or on_precommit stands until the host makes another; a C table fixes both once, at strategy_native_host_create_v1, which moves no value. |
C_V1_EXCLUSIONS["host_minor"] |
| the C++ library around the host | Spec and curve validation and digests (validate_native_run_spec, normalize_native_run_spec, the subscription and auxiliary-feed validators, native_run_spec_digest and the other digests, validate_native_fx_curve, native_fx_curve_digest); the input preflight (preflight_native_inputs, native_bar_structurally_valid); the calendar utilities of native_calendar.hpp (the civil-date pair native_civil_date / native_civil_days and pairing_aggregates among them, since R5 lane H-THIN); the request helpers (quantity_on_grid, market_request); the module export macro of native_module.hpp; the run identity a RequestHandle carries. A C host validates by configuring (strategy_configure_native_ext_result_v1 leaves the handle unconfigured on a refusal) and has no digest. The kernel's own machinery (WorkingRequestCore, INativeDriverSink) is no host surface in either language. |
C_V1_EXCLUSIONS["library_facilities"] |
native_toolkit.hpp |
Header-only C++ over the host's own commands: submit_bracket and its receipt, and the OrderBook<Key> id book. A C host writes the same commands itself: each bracket leg is a strategy_native_submit_v1 with PF_NATIVE_OWNER_WAIT_FOR_APPLIED and a PF_NATIVE_GROUP_MEMBER group, answering its own refusal, and an id book is its own key-to-handle map over _submit_v1 / _replace_v1 / _cancel_v1. |
C_V1_EXCLUSIONS["toolkit"] |
Every policy hook has a C callback since R5 lane F4:
validate_execution_precommit is on_precommit, resolve_anchored_level
on_anchored_level, the price and shape half of resolve_execution_terms
on_execution_terms, declare_auxiliary_feed
strategy_native_declare_auxiliary_feed_v1 and native_sized_units
strategy_native_sized_units_v1. A callback carries the facts and answers its
C struct declares; what it leaves out is in the table above. A table that leaves on_bar_open or
on_precommit out declares that hook absent when the host is created
(declare_native_bar_open_hook / declare_native_precommit_hook, R5 lane
D2-A), which moves no value.
The same strategy through both surfaces (R5 lane H-MEASURE).
tests/test_native_acid_composite.cpp drives one Pine-free strategy through
every feature of the G1 acid test — a fee-reserved Sized entry, anchored
bracket legs on a price grid, a keep_binding re-issue, a trail in ticks, a
maintenance-only margin model and its liquidation seen through the open lots,
an FX step, a risk limit and its refusal, calc-on-fills, an hourly and an
auxiliary subscription, session-day flags, a partial close across lots and an
exact-sum close, a typed refusal, the magnifier counters, the kernel-recorded
report — in batch and in stream, and runs its C port
(tests/test_native_acid_composite_c.c, configured through the typed
strategy_configure_native_ext_result_v1) in the same process: every shared
ledger fact is identical, per-bar broker-state hash rows included, within one
driving mode (a batch and a stream record the same trades, fills, equity
curve, series deliveries and check points, and different hash rows, as the
driving-mode rules above say). What the C port cannot read is named family by
family: the four items ruled to 1.1.0 — a non-mutating execution preview
(inspect_current_execution), the applied event's origin and label, a closed
trade's entry comment, and a C ReplaceOptions word (keep_handle /
keep_binding / retain_trigger_state) — plus closes_session_day_open_ended
(the asymmetry the COVERAGE block records), the protected
broker_state_hash_from_execution_hash fold, and two C++ readouts with no C
field: NativeCurrentPointView::quote_origin_ordinal and
NativeDecisionContext::driver_statistics. scripts/check_native_c_api_surface.py
proves the block is exactly that class's public surface and runs as a source
guard in every ci_verify.py profile, so the list cannot silently go stale;
scripts/test_check_native_c_api_surface.py proves the guard can fail. A
second list, BASE-CLASS SEAMS, is the same census for the members of the
base a host is documented to call or override from its callbacks, opted in
one by one by a @host-seam line in engine.hpp: today the entry-bar mask
declaration and hash_host_extension (both spelled, the second as
on_hash_extension) and its deprecated spelling hash_source_extension
(excluded: C only ever had the current spelling).
Errors and hardening. Every struct is tagged and size-prefixed
(struct_size, version); a mismatch is PF_NATIVE_E_STRUCT, an enumerator
outside its enumeration is PF_NATIVE_E_TAG, and neither mutates anything.
A C callback must not unwind; returning non-zero latches
NativeFailureCode::CallbackException (PF_NATIVE_FAILURE_CALLBACK, 5) and
the run ends Failed, readable through strategy_native_state_v1.
tests/test_native_c_api_frozen_header.cpp compiles a fixture against the
byte-frozen v1 copy of the header in the private fixture set native_c_api/v1 and
links it against the current runtime, so an unnoticed layout change shows up
as the refusal the header promises rather than as silent misreading.
Events. strategy_native_events_v1 flattens native_events() into one
tagged POD: the nineteen CommandEvent alternatives, plus the driver point
and the account observation — the rows the run's retention keeps. A C host
under PF_NATIVE_EVENT_RETENTION_WINDOW acknowledges what it read with
strategy_native_acknowledge_events_v1 (from on_run_begin with 0, then its
cursor) and reads where the window starts with
strategy_native_event_window_v1, the C spellings of
native_acknowledge_events and native_event_window_start. One kind named in the design is not
represented and never appears: a completed higher-timeframe bucket, which is
delivered through the on_timeframe_bar callback and never recorded in the
event history. Ordinals are non-decreasing rather than strictly increasing —
an applied execution and the account observation it produced share one — so a
page never ends in the middle of such a group and a poller can advance by the
last returned ordinal.
NativeRiskEvent is the nineteenth alternative and arrived after this
header froze, so it took a tag of its own past the two observations —
PF_NATIVE_EVENT_RISK (21) — rather than taking 19 and renumbering
PF_NATIVE_EVENT_DRIVER_POINT and _ACCOUNT. A reader compiled before it
skips it by tag, which is how any unknown tag must already be treated, and
only ever meets it in a run whose spec declares risk — a spec that reader
cannot write. The event names no request, because the block it opens is an
account fact: incarnation stays 0, reason is the
pf_native_risk_limit_e that breached, price is the observed value,
raw_price the limit it reached (already resolved against its basis equity
when the limit was a percent), cycle_before the risk day on the spec's own
day basis, successor the cursor's matching-point ordinal, and the cursor
fields are set. There is no new callback: the history is the delivery path,
so pf_native_callbacks_v1 does not move.
These are contract, not pending work.
- No broker-open FX epoch clock, and none is needed. The declared
NativeFxCurveis the run's FX epoch, a step of it is theFxRollmargin check point, and a confirmed-bar stream runs under it as a batch does (@ref native_engine_stream_fx). Tick-driven input under a declared curve stays refused, for the conversion-clock reason stated there. - The precommit verdict is not previewed.
inspect_current_executionreports typed terms outcomes — facts about the proposed terms — and never the host's ownvalidate_execution_precommitverdict, which runs once, on the physical attempt. - A series finer than the input needs a declared feed. Without
NativeRunSpec::auxiliary_feeda subscription finer thaninput_tfis refused at configure rather than emulated. Another symbol is addressable only through a feed the host installs (NativeRunSpec::instrument_feeds, historical runs only); there is no chart-slice mapping. - TradingView's
request.securityrules are not reachable from a bare host. The lookahead merge latch, the calling-bar publication gates, Heikin-Ashi substitution, the range-start cut andrequest.security_lower_tfemulation are source-layer code, and none of them can change a subscription's buckets. - The adapter leaves ten capabilities outside its declaration surface — nine
are native-only or adapter-policy (
price_grid/grid_rounding,risk,max_abs_units,max_open_lots,initial_margin_fraction,report_open_position_at_end,open_bar_view,auxiliary_feed,quantity_tolerance) and one is an adapter-hook (subscriptions) — each with a ruling and an executed consumer, with the measurement that decided it:docs/adr/0001-kernel-adapter-boundary.md("Kernel capabilities the Pine adapter does not declare") anddocs/design/native-feature-parity.md§3.6 / §3.7.
The standalone native host has no Pine decision path at runtime, and the
constructor/member cut has since landed: engine.hpp has zero references to
CapAttachment, OrderPriority or IntradayCap. NativeStrategyHost is
zero-argument (native_host.hpp:845); the CapAttachment constructor belongs
to source::PineStrategyHost (pine_strategy_host.hpp:286-289), and the cap
type itself lives in the adapter (IntradayCap intraday_cap.hpp:84).
Nor is there a build-level one. The two source sets are disjoint:
PINEFORGE_SOURCE_LAYER_SOURCES (CMakeLists.txt:91) holds all six
src/compat/pine/ units and all twelve src/source/ ones, and
PINEFORGE_KERNEL_SOURCES (CMakeLists.txt:114) holds the kernel's own
thirty-five, which are what add_library (CMakeLists.txt:158) compiles into
pineforge_kernel. libpineforge.a still carries both sets when
PINEFORGE_BUILD_SOURCE_LAYER is ON, which is the default; the kernel archive
exists either way. The installed-header closure is clean too, which the
independence checker proves
(ROOT_HEADERS check_native_include_independence.py:392). See
docs/adr/0001-kernel-adapter-boundary.md.
-DPINEFORGE_BUILD_SOURCE_LAYER=OFF (default ON) configures a build with
no source-adapter translation unit compiled at all:
cmake -S . -B build-kernel -DCMAKE_BUILD_TYPE=Release \
-DPINEFORGE_BUILD_SOURCE_LAYER=OFF
cmake --build build-kernel -jThe default build is unchanged: libpineforge.a still contains every kernel
object plus the source layer, and PineForge::pineforge still links a Pine
host. What the option adds is the kernel target itself, which exists in both
builds:
find_package(PineForge REQUIRED)
target_link_libraries(my_native_consumer PRIVATE PineForge::kernel)PineForge::kernel (libpineforge_kernel.a) is built from
PINEFORGE_KERNEL_SOURCES with exactly the flags pineforge uses. It does not
mention the source layer at all — the rich begin bridge carries its host
overrides as an opaque const void* — so it links standalone;
scripts/check_native_include_independence.py --kernel-archive asserts that
with nm over the archive's defined and undefined symbols, with no whitelisted
symbol.
With the option OFF the build excludes, each with a CMake STATUS line:
src/source/*.cppandsrc/compat/pine/*.cpp, solibpineforge.aholds the kernel objects alone (equivalent tolibpineforge_kernel.a);- the installed
include/pineforge/source/andinclude/pineforge/compat/headers, which would otherwise declare functions with no definition; - every Pine-bound target: the corpus and bench strategies, the tutorial
strategy, and the live runner's
runner/examples/strategy.cpp(native-market-exampleandnative-selected-examplestill build); - every test translation unit that reaches a
pineforge/source/orcompat/pine/header, and the receipt-backed ABI rows whose pairing TU derives fromPineStrategyHost. The remaining CTest rows all run.
A kernel witness therefore never shares a translation unit with its adapter
twin: the native half is source-free, the twin lives in a sibling
tests/test_<feature>_twin.cpp that the reach check drops, and what both need
sits in a source-free tests/<feature>_fixture.hpp. The feature suites follow
that split — test_native_report_truth, test_native_margin_hooks,
test_native_calc_timing, test_native_htf_subscriptions,
test_native_price_grid and test_l11a_host_excursion each run kernel-only
with their twin beside them — so the kernel-only profile proves the kernel's
own features, not only the rows that happened to be source-free.
python3 scripts/ci_verify.py kernel is the profile that verifies the
kernel-only build (Release, live runner ON, tutorial OFF, source layer OFF);
CI runs it as the kernel-only job. The profile carries a row floor, its
number in the PRIVATE_FIXTURE_PROFILE_FLOORS ci_verify.py:794 table in
scripts/ci_verify.py: the count the kernel-only CTest set is
expected to run with the private overlay present, minus the pinned
private-fixture rows of tests/private_fixture_rows.txt it registers as SKIP
placeholders while the overlay is absent. The ctest-floor stage fails the
run when fewer rows
ran or CTest printed no count it can read, so a test TU that silently becomes
source-bound (or a filter that empties the suite) is a refusal rather than a
smaller green. A row counts only when its test ran to a verdict: a row CTest
skipped (test_native_live_websocket exits 77 wherever libcurl lacks
WebSocket support) or could not start is listed beside the count, in
ci-logs/ctest-floor.log and as ctestSkipped / ctestNotRun in
ci-summary.json, and never counted, so a row that stops executing fails the
floor as well. The release profile carries the same gate with its own row in
the table, so a row that leaves the default build fails CI too.
When a row lands, re-measure the profile's own overlay-present Lab run and
edit the table; --min-tests N overrides the floor for one run
of any profile (debug, sanitizers and native carry no default floor).
An audit of strings libpineforge_kernel.a for Pine / TradingView vocabulary
is now a gate:
scripts/check_kernel_residuals.py --archive build-kernel/lib/libpineforge_kernel.a
scans what a consumer LINKS and only that — the symbol table (nm -C over the
archive, defined and undefined, demangled) and the string literals (strings -a
over a copy whose debug information has been stripped with objcopy --strip-debug, llvm-objcopy --strip-debug or the platform strip -S, and
exit 2 when the host has none of them) — so the verdict is the same in every
build type: a -g archive's DWARF names every block-scope local, every struct
member and every source path, and none of those is a residual surface. It
matches a fixed residual vocabulary against whole identifiers only: at least
three bytes, delimited by non-identifier bytes, so machine code strings prints
as C0"TV" is not a name. It reads the installed headers and the kernel's own
string literals as well. The vocabulary (an identifier containing pine other
than pineforge, tradingview, barmerge, coof, pooc,
market_admission, calc_on_order_fills, process_orders_on_close or a tv
segment, a strategy() parameter word, syminfo, barstate, the underscore
spelling of a Pine member, the camel-case Tv / TV spelling, a bare
islastbar / isfirstbar, heikinashi, renko or tradingday; a text
containing strategy.<name>, ta.<name>, request.security,
barmerge.<name>, a dotted session. / barstate. member, another Pine
namespace such as timeframe., input., str., math., matrix. or
xloc., the word Pine, or a __name__ label, where <name> is not a C/C++
file suffix — ta.ema is a call, ta.hpp is this project's header, and a
sanitizer build writes every source path into rodata as a real literal)
requires every match to be listed in the first column of the ADR's residual
tables — a name by its name, a text by its exact words, so a ruled name never
covers a text that contains it — and every listed match to still be in the
archive, the installed headers or a kernel source literal. A row that says
"no archive symbol" is held to it, and the two counts the key line prints
(the rulings the vocabulary reads) have floors in scripts/ci_verify.py
(ADR_RULED_IDENTIFIERS_MIN, ADR_RULED_TEXTS_MIN). The kernel profile runs it as the
kernel-residuals stage right after the build; every profile runs it as the
CTest row test_kernel_residuals (with the checker's own must-fail
self-tests). What remains is listed with its ruling in
docs/adr/0001-kernel-adapter-boundary.md ("Residual TradingView-named
surface"); the short version:
- Neutral spellings. The kernel's public utility names are the neutral
ones:
NumericMatrix/GenericMatrix<T>(matrix.hpp,generic_matrix.hpp),timeframe_time/local_hour/session_in_market(session_time.hpp),str_format/str_tostring(str_utils.hpp),deterministic_random(math.hpp) andfloat_band_eq… (ta_compare_band.hpp). ThePine*/pine_*spellings are exact deprecated aliases kept for generated code; thepine_float_*shim ships only with the source layer (include/pineforge/source/pine_float_compare.hpp). - Frozen pending-row field names. The seventeen
pine_exit_activation_*,pine_frozen_market_instruction_*andpine_birth_reachreflection strings,tv_carry_qty, the sevencoof_*(calc-on-order-fills), the threepooc_*(process-orders-on-close) and the seventy-eightmarket_admission_*names (the frozen mirror of the source-layer admission journal) belong to the frozenpf_pending_order_v1_tmirror, an append-only C ABI contract published bystrategy_pending_order_layout. The kernel translation unit holds each reflection row — name, C type, offset, size — and never a value: the values are projected by the source layer alone, and a bare host'sstrategy_pending_orders_lenis 0. The neutral view isnative_working_requests(). Each family has its ADR row, listing every name, and the gate above holds the list. - Deprecated public spellings, removed for 1.0
(
docs/adr/0001-kernel-adapter-boundary.md, "Deprecated public spellings" — a separate table, outside the residual section the gate parses, because none of these names is a symbol or a literal in the archive). The C ABI'spf_equity_stats_t::sharpe_monthly/sortino_monthlyare spelledsharpe_tv/sortino_tvin v0.13.1, the last 0.x release: the samedoubleat the same offset; in v1.0.0 the old spelling is gone, nothing an FFI consumer links or reads moved, and the serialized report key stayssharpe_tv. The standalonelifecycle_v1enumeratorsexit_legs::Domain::FillRecalc/MagnifierFillRecalc(the fill-recalculation re-entry pass) were also spelledCoof/MagnifierCoofin development builds before the removal (no tagged release has those two spellings), with the same values.test_removed_public_spellingsholds that each old spelling fails to compile. - The ambient EMA seeding default.
ta::EMAseeds from its first finite input (EmaSeeding::FirstValue, the default) or from the simple average of its firstlengthinputs (EmaSeeding::SimpleAverage, na for the warm-up window the wayRMAandSMAwarm up). A host names the seeding per instance,ta::EMA ema(length, ta::EmaSeeding::SimpleAverage), and touches no global.ta::ema_na_warmup_flag()is the calling thread's ambient default (held in its runtime block: the thread's own, or the running pump's while one runs) that an instance naming no seeding latches on its firstcompute(); the Pine adapter raises it around one evaluation context under its opt-in run flags, the way any host may. It is ruled in the ADR as a generic ambient indicator option (a mechanism the vocabulary gate cannot see); nothing in the kernel raises it. - Host-only modes behind frozen C setters.
strategy_set_realtime_tailandstrategy_set_probe_suppress_tail_logicare the Pine source host's live-probe protocol. On a bareNativeStrategyHostthe kernel's virtual seamsset_realtime_tail/set_probe_suppress_tail_logicare accepted and inert, as the C setters always were on a native module: the C++ seam answersfalse("no such mode here"),last_error()stays empty, and the run is byte-identical to one that never asked. request.securityin three feed-store messages. The last_error texts ofset_native_security_feedname the feature the frozen C exportstrategy_set_native_security_feeddocuments; the mechanism is the generic authoritative-feed store above.
pineforge-live is optional (-DPINEFORGE_BUILD_LIVE_RUNNER=ON, default
OFF). That option also builds native-market-example and
native-selected-example. Native modules
need --native-config FILE instead of --input / --override / --syminfo
(those flags are refused). The file is a strict JSON object: unknown and
duplicate keys fail; every listed key is required; there are no
environment defaults for spec fields. Omitted CLI clock/symbol flags take the
file. Explicit --input-tf, --script-tf, --timezone, --session,
--chart-timezone, or --symbol must equal the file.
input_mode is the CLI --mode (bars or ticks), not a JSON key.
Timestamps in the spec, warmup CSV, and JSONL are Unix milliseconds.
Complete file (optional numeric payloads are JSON null, not omitted keys).
fee_kind and close_execution are the C++ enumerator names.
allowed_open_directions is the numeric mask (3 = Both; see
NativeOpenDirections).
{
"run": {
"session_key": "native-guide",
"run_number": 1
},
"clock": {
"input_tf": "5",
"script_tf": "5",
"timezone": "UTC",
"session": "24x7",
"chart_timezone": ""
},
"instrument": {
"ticker": "MOCK",
"tickerid": "TEST:MOCK",
"type": "crypto",
"currency": "USDT",
"basecurrency": "ETH",
"description": "",
"volumetype": ""
},
"execution": {
"initial_capital": 10000,
"point_value": 1,
"account_fx": 1,
"price_tick": 0.01,
"slippage_ticks": 0,
"fee_kind": "CashPerExecution",
"fee_value": 6,
"quantity_grid": null,
"close_execution": "NextEligiblePoint",
"max_abs_units": null,
"max_open_lots": null,
"allowed_open_directions": 3,
"initial_margin_fraction": null
}
}Save the configuration as native-run.json. Warmup uses the configured
calendar. For this example, save one confirmed bar as history-5m.csv:
timestamp,open,high,low,close,volume
0,100,102,99,101,4Save these subsequent bars as events.jsonl. The first callback occurs
during warmup; the opening and flatten orders fill on subsequent inputs.
{"type":"bar","bar":{"ts_open":300000,"o":102,"h":103,"l":101,"c":102,"v":4}}
{"type":"bar","bar":{"ts_open":600000,"o":103,"h":104,"l":102,"c":103.5,"v":4}}
{"type":"bar","bar":{"ts_open":900000,"o":103.5,"h":104,"l":103,"c":103.5,"v":4}}
{"type":"bar","bar":{"ts_open":1200000,"o":104,"h":105,"l":103.5,"c":104,"v":4}}cmake -S . -B build-live \
-DCMAKE_BUILD_TYPE=Release \
-DPINEFORGE_BUILD_LIVE_RUNNER=ON
cmake --build build-live -j --target pineforge-live native_market_example
build-live/bin/pineforge-live run \
--strategy build-live/lib/native-market-example.so \
--native-config native-run.json \
--warmup history-5m.csv \
--feed events.jsonl \
--mode bars \
--ledger orders.sqlite3 \
--webhook-url https://receiver.example/order-actions \
--name native-market--mode ticks is the other input mode. Tick JSONL uses ts (Unix ms),
positive seq, price, qty. Confirmed-bar JSONL uses
bar.ts_open (Unix ms) plus o,h,l,c,v. {"type":"time","ts":…} is tick
mode only. Mixing modes is refused. Monthly input_tf is refused on this
path before the process continues into the run.
Other CMake options (unchanged defaults): PINEFORGE_BUILD_TESTS ON,
PINEFORGE_BUILD_TUTORIAL ON, PINEFORGE_BUILD_EXAMPLES OFF (ON in the
release and kernel CI profiles), PINEFORGE_ENABLE_SANITIZERS OFF,
PINEFORGE_STRICT_WARNINGS OFF. The
runner also needs SQLite3, libcurl 7.86+, and OpenSSL Crypto.
The C ABI query strategy_execution_contract identifies NativeMarketV1; the
runner then uses strategy_configure_native_v1 to apply its versioned run
specification. strategy_configure_native_fx_curve_v1 additionally stages an
immutable curve on a Ready native handle (or clears it with n == 0); invalid
pointer/size/phase/curve input returns -1 without mutation. The legacy
strategy_set_account_currency_fx_series still stages a series on a native
handle while no run is active: a batch converts at it, stream_begin refuses
it (see Stream FX above), and a call during a run latches Failed
(UnsupportedSource). Prefer the C++ NativeRunSpec and NativeFxCurve shown here over
hand-maintaining either versioned C struct.
For enumerator payloads, pairing names, session grammar, and failure codes, read the headers cited above.