From 44f7d0f8e6692464b2f7d73e2bf0f968f27ebb72 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 12:46:57 +0800 Subject: [PATCH 1/4] Deliver a native run's report only for the call that began it (onto #356) One integration commit on main 70ddaa3a7 (recorded outputs, #356): the net change 31a40a14b..d83ab7d4a of px/native-attempt-fix-20261009, 15 files, applied as a squash of that range. The author branch and its history are untouched. Supersedes 3d2f3f4 (the per-entry Completed gate): the reader gate 86a1856, its revert 124a434 and the restore ec9047d are folded into this net diff, so the documentation 3d2f3f4 wrote is carried here in its final form and this commit removes no documentation sentence of main. What changes (unchanged from the author head): - BacktestEngine::fill_report publishes the retained rows only while the latest run or stream_begin call began (reached reset_run_state). A call refused without beginning (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, and strategy_native_run_v1 answers PF_NATIVE_E_RUN_FAILED for it instead of handing out the earlier run's report. - NativeExecutionConsumer keeps two bools, rows_current_ and batch_current_, behind a private RAII AttemptScope opened first by run_simple, run_tf, run_rich and stream_begin. strategy_stream_fill_report goes through native_stream_snapshot_report and fill_snapshot_report, so a live stream's rows survive a batch call refused over it, which reads empty. - No engine.hpp, ReportC or public API layout change, no codegen change, no version bump. The two bools are report-presentation bookkeeping and are not hashed; identity of the state hashes is owed on the parity population. - Tests kept: the 14 repro assertions (RED on 31a40a14b) and the old, twin, current-partial and live-stream rows in tests/test_native_c_api.c, test_checked_settings.cpp and test_native_example_batch.cpp; one obsolete row in test_run_failure_codes.cpp (a second call on a Failed handle is refused, 0 trades). Integration against #356: - One textual conflict, docs/design/native-feature-parity.md rows OT6 and OT7 (adjacent lines changed by different sides). Kept #356's OT6 ("73 runtime exports") and the author's OT7 anchor (native_execution_consumer.cpp:7636). Nothing from either side was dropped. - The other five files #356 also changed (ADR 0001, native-engine.md, native_c_api.h, c_abi.cpp, engine_report.cpp) merged cleanly and keep every recorded-output implementation and doc line. - Six documentation anchors that the author's insertions moved, on lines the author's diff left alone, are carried by an exact line map: parity page lines 126, 159 and 1679, native-engine.md line 647, pine-to-native.md lines 129 and 466. - Not built, not run and not anchor-checked here (no compiler or project helper on this lane); the verification runbook is in the lane report. Co-Authored-By: Claude Sonnet 5.5 --- docs/adr/0001-kernel-adapter-boundary.md | 42 +- docs/design/native-feature-parity.md | 116 +-- docs/native-refactor-progress.md | 8 +- docs/pages/native-engine.md | 37 +- docs/pages/pine-to-native.md | 58 +- include/pineforge/native_c_api.h | 23 +- src/c_abi.cpp | 6 +- src/engine_report.cpp | 20 + src/native_c_host.cpp | 13 + src/native_execution_consumer.cpp | 64 ++ src/native_execution_consumer.hpp | 24 + tests/test_checked_settings.cpp | 139 ++++ tests/test_native_c_api.c | 886 +++++++++++++++++++++++ tests/test_native_example_batch.cpp | 31 + tests/test_run_failure_codes.cpp | 2 +- 15 files changed, 1343 insertions(+), 126 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index cd686a052..8fb53ca4d 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -78,7 +78,7 @@ summary of it. (`native_host.hpp:893`); also `on_native_input` (`native_host.hpp:875`), `on_native_tick` (`native_host.hpp:878`), `on_native_timeframe_bar` (`native_host.hpp:885`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7420-7422`) — + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7434-7436`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:913`), `on_native_sub_bar` (`native_host.hpp:928`) and post-fill `on_native_applied` (`native_host.hpp:938`). `on_bar` is `final` (`native_host.hpp:857`). @@ -112,7 +112,7 @@ summary of it. bare host needs no override at all, while `HostSized` hands the units to the host — and an unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:947`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:5278`). + (`TermsUnresolved` `native_execution_consumer.cpp:5292`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1335`). Do *not* implement @@ -123,7 +123,7 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:5251`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:5265`) | | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -172,7 +172,7 @@ summary of it. (`guard_native_mutation` `engine_aux_security.cpp:83`). Indicators are `ta_*.cpp`, session/calendar `session_time.cpp` / `native_calendar.cpp`, FX `configure_native_fx_curve` (`native_host.hpp:1202`). -- **Reporting.** `fill_report` (`engine.hpp:1859`, `src/engine_report.cpp:40`) gives a bare host +- **Reporting.** `fill_report` (`engine.hpp:1859`, `src/engine_report.cpp:56`) gives a bare host trade stats, and the equity curve is a run-spec choice rather than a host chore. `NativeReportPolicy::HostRecorded` (the default) leaves the series to the host, whose `record_equity_point` / `update_equity_extremes` remain protected @@ -182,7 +182,7 @@ summary of it. `report_open_position_at_end` books the still-open position as a mark-to-market row under `KernelRecorded`. Under `HostRecorded` an unrecorded curve still degenerates — `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:203`, - consumed by `fill_metrics_section`, `src/engine_report.cpp:118-125`) — which is why the policy exists. + consumed by `fill_metrics_section`, `src/engine_report.cpp:138-145`) — which is why the policy exists. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` (`native_host.hpp:1376`) returns the consumer's event/continuation hash. **A digest names the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* — @@ -427,10 +427,10 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `scripts/check_c_abi_runtime.py` pins by name, and `src/native_c_host.cpp` implements the **43** additive symbols of ``. A C host creates a host from a callback table (`strategy_native_host_create_v1` `native_c_api.h:2682`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2695`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2723`), `_replace_v1` - (`native_c_api.h:2745`), `_cancel_v1` (`native_c_api.h:2785`), `_cancel_all_v1` (`native_c_api.h:2789`), `_cancel_where_v1` (`native_c_api.h:2810`), - `_execute_current_v1` (`native_c_api.h:2831`) — under the kernel's own legality rule. Streaming needs no new + (`strategy_native_run_v1` `native_c_api.h:2714`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2742`), `_replace_v1` + (`native_c_api.h:2764`), `_cancel_v1` (`native_c_api.h:2804`), `_cancel_all_v1` (`native_c_api.h:2808`), `_cancel_where_v1` (`native_c_api.h:2829`), + `_execute_current_v1` (`native_c_api.h:2850`) — under the kernel's own legality rule. Streaming needs no new symbol: the whole `strategy_stream_*` family takes that handle unchanged. The header's COVERAGE block carries one line per public member of `NativeStrategyHost` with either its C spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the @@ -743,7 +743,7 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| | `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2221`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | -| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3096`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3115`), reading a SIZED request's own sizing block | the sizing-query scenario | | `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2607`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | | `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2616`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | | `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2623`) | the anchored-level scenario | @@ -753,18 +753,18 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2789`). | +| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2808`). | | `cohort_add` (`native_host.hpp:1285`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:906`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | -| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3068`). | +| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3087`). | | `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1718`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | | `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:661`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | | `strategy_set_path_order` (`pineforge.h:941`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:904`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | | the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1036`) | **no C names in the header; generator-owned** | The mirror is generated from the Pine source host's resting-order record by `scripts/gen_pending_order_mirror.py` (held by its `--check` source guard) and self-described at run time by `strategy_pending_order_layout`. Its enumerated fields are the source layer's order vocabulary, not a kernel enumeration. A C name for them belongs in that generator, which owns the struct, so that it cannot drift from the mirror — never hand-written beside it. | -| `strategy_configure_native_v1` (`c_abi.cpp:930`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:4053`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | +| `strategy_configure_native_v1` (`c_abi.cpp:930`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:4066`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:546`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:8132-8144`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1098-1103`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:65-66`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:8146-8158`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1098-1103`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1912`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | | `strategy_outputs_set_enabled` and the eight `strategy_outputs_*` readers (`pineforge.h`, group `pf_outputs`) | **additive runtime exports** | Recording is off by default, and nothing a run computes reads the record. The event copy is size-prefixed like `strategy_pending_order_get`, and `PF_ABI_VERSION` stays 4. The writers are C++ only in this version (the BASE-CLASS SEAMS rows of `native_c_api.h`): a C host declares no outputs, so its handle answers every reader as empty. | @@ -823,7 +823,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:9167-9170` (lookahead) and `clear_if_gapped` `:9156-9159` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:9181-9184` (lookahead) and `clear_if_gapped` `:9170-9173` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2271`, `::gaps` `include/pineforge/native_c_api.h:2275` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:799`, `pf_native_gaps_e` `:810`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | @@ -837,11 +837,11 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:7622`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar, read at the bar's label, or at its interval's first eligible instant when that is later (a bar opening in a declared break), never at an instant before the label; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone. Reading a bar at its own label is generic too: an instant before the label is not the bar's, whatever grid the calendar keys its interval to (R5 lane K-SESSION-WINDOWS: NASDAQ:AAPL with extended hours opens its bars on the hour, and the 16:00 bar, which the 09:30-anchored grid files under 15:30, read in session and closed the regular day in place of the 15:00 bar; TSE:7203's and CBOT:ZC1!'s 60-minute bars that open inside a break are in session on TradingView's tapes, which reading at the label alone would lose) | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), `tests/test_native_session_day_bar_open.cpp` (kernel-only: TradingView's extended-hours and break-open tapes, `tests/fixtures/session_windows`), the C scenario of `tests/test_native_c_api.c`, `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag), `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point), and `tests/test_native_session_day_zoned.cpp` (kernel-only: on a certified cycle of an exchange zone, which `native_calendar::cycle_certificate` decides and the witness checks at real transitions, the facts read an instant off its session day, and match the zone's POSIX twin at every decision point) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:8366-8382` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8334-8355` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6923` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host'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 (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | -| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6648` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | +| `present_session_day` `src/native_execution_consumer.cpp:7636`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar, read at the bar's label, or at its interval's first eligible instant when that is later (a bar opening in a declared break), never at an instant before the label; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone. Reading a bar at its own label is generic too: an instant before the label is not the bar's, whatever grid the calendar keys its interval to (R5 lane K-SESSION-WINDOWS: NASDAQ:AAPL with extended hours opens its bars on the hour, and the 16:00 bar, which the 09:30-anchored grid files under 15:30, read in session and closed the regular day in place of the 15:00 bar; TSE:7203's and CBOT:ZC1!'s 60-minute bars that open inside a break are in session on TradingView's tapes, which reading at the label alone would lose) | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), `tests/test_native_session_day_bar_open.cpp` (kernel-only: TradingView's extended-hours and break-open tapes, `tests/fixtures/session_windows`), the C scenario of `tests/test_native_c_api.c`, `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag), `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point), and `tests/test_native_session_day_zoned.cpp` (kernel-only: on a certified cycle of an exchange zone, which `native_calendar::cycle_certificate` decides and the witness checks at real transitions, the facts read an instant off its session day, and match the zone's POSIX twin at every decision point) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:8391-8407` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:8348-8369` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6937` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host'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 (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6662` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:358`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1601`, `:1642`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2333`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:208` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | @@ -941,7 +941,7 @@ questions: the mechanism, why it is generic, and why no knob is needed. | Kernel capability | Generic mechanism | Why no platform knob is added | Executed witness | |---|---|---|---| -| A daily bar the installed daily feed dates: `NativeExecutionConsumer::prepare_day_labels` `src/native_execution_consumer.cpp:2067`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:2660`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2017`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2120`) | (a) The venue's own daily bars date its days. When an intraday input aggregates into `"D"` script bars and a daily feed is installed -- the feed store's (`set_native_security_feed`) or a declared series' authoritative bars -- each stamp labels the D bar holding the session instant it covers: the stamp itself inside a session, else the next session's open, the instant the store already keys the feed's bars by. Only the label moves; the bar's span, closes, successor and OHLCV stay the calendar's aggregate, and every lookup by the label -- the bar's open and close, its sub-bars on a magnified run, its execution anchors, its risk day and a C host's session day -- reads the bar's own interval and day, not the day before the stamp. A bar no stamp covers keeps the calendar's label, a bar two stamps cover takes the earlier, and a label never passes an input bar the run holds: a stamp with an input between it and its bar's calendar open labels nothing, so a label never follows its bar's first input nor precedes another bar's. A stamp at the calendar's own open (09:30 ET, 17:00 CT, 00:00 UTC, OANDA:EURUSD's 17:00 ET) changes nothing; a venue that dates its days elsewhere -- OANDA's XAUUSD at 17:00 ET in the break of its 1800-1700 session, NSE's Muhurat day at its 13:45 open -- is driven by installing its own daily bars, and the kernel holds no venue's day-stamp convention | (b) None: the policy knob is the authoritative-feed ruling's, installing the daily feed or not (`engine_aux_security.cpp`'s contract), exactly as W15's late opens are; a "day label" field would restate the stamps the run installed. No input moves without the feed, and a calendar input's labels (the venue's bars themselves) never move. Calibration: TradingView dates every order of OANDA:XAUUSD 1D, backtested with the bar magnifier, by the 17:00 ET stamp -- 22:00 UTC in EST, 21:00 UTC in EDT -- where the 1m aggregate's session opens at 18:00 (`lab tv` `xc-dmag-xau1d`, `xc-dmag-dip-xau1d` and their `-off` twins, `tests/fixtures/daily_magnifier`) | `tests/test_native_day_labels.cpp` (kernel-only: break stamps across a DST switch, the bar's open reading its close's interval and input slot, a magnified run's sub-bars, a declared series' authoritative bars, stamps at the open, a missing, a doubled and a late stamp, an input in the break before a stamp's bar, NSE's late open, a daily, weekly and intraday script control, a reused host), `tests/test_native_c_api.c` (a C host's session day of a dated bar), `tests/test_daily_magnifier_tapes.cpp` (the four tapes through the Pine adapter: 20/20, 18/18, 31/31, 31/31; without the daily feed every trade an hour late) | +| A daily bar the installed daily feed dates: `NativeExecutionConsumer::prepare_day_labels` `src/native_execution_consumer.cpp:2079`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:2674`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2029`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2132`) | (a) The venue's own daily bars date its days. When an intraday input aggregates into `"D"` script bars and a daily feed is installed -- the feed store's (`set_native_security_feed`) or a declared series' authoritative bars -- each stamp labels the D bar holding the session instant it covers: the stamp itself inside a session, else the next session's open, the instant the store already keys the feed's bars by. Only the label moves; the bar's span, closes, successor and OHLCV stay the calendar's aggregate, and every lookup by the label -- the bar's open and close, its sub-bars on a magnified run, its execution anchors, its risk day and a C host's session day -- reads the bar's own interval and day, not the day before the stamp. A bar no stamp covers keeps the calendar's label, a bar two stamps cover takes the earlier, and a label never passes an input bar the run holds: a stamp with an input between it and its bar's calendar open labels nothing, so a label never follows its bar's first input nor precedes another bar's. A stamp at the calendar's own open (09:30 ET, 17:00 CT, 00:00 UTC, OANDA:EURUSD's 17:00 ET) changes nothing; a venue that dates its days elsewhere -- OANDA's XAUUSD at 17:00 ET in the break of its 1800-1700 session, NSE's Muhurat day at its 13:45 open -- is driven by installing its own daily bars, and the kernel holds no venue's day-stamp convention | (b) None: the policy knob is the authoritative-feed ruling's, installing the daily feed or not (`engine_aux_security.cpp`'s contract), exactly as W15's late opens are; a "day label" field would restate the stamps the run installed. No input moves without the feed, and a calendar input's labels (the venue's bars themselves) never move. Calibration: TradingView dates every order of OANDA:XAUUSD 1D, backtested with the bar magnifier, by the 17:00 ET stamp -- 22:00 UTC in EST, 21:00 UTC in EDT -- where the 1m aggregate's session opens at 18:00 (`lab tv` `xc-dmag-xau1d`, `xc-dmag-dip-xau1d` and their `-off` twins, `tests/fixtures/daily_magnifier`) | `tests/test_native_day_labels.cpp` (kernel-only: break stamps across a DST switch, the bar's open reading its close's interval and input slot, a magnified run's sub-bars, a declared series' authoritative bars, stamps at the open, a missing, a doubled and a late stamp, an input in the break before a stamp's bar, NSE's late open, a daily, weekly and intraday script control, a reused host), `tests/test_native_c_api.c` (a C host's session day of a dated bar), `tests/test_daily_magnifier_tapes.cpp` (the four tapes through the Pine adapter: 20/20, 18/18, 31/31, 31/31; without the daily feed every trade an hour late) | ## Boundary rules (for contributors) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 4e6a4e49c..ca6f539f6 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,7 +47,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3293-3304; `entry()` pine_adapter.cpp:6088, `order()` pine_adapter.cpp:14395) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5961-6008; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3293-3304; `entry()` pine_adapter.cpp:6088, `order()` pine_adapter.cpp:14395) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5975-6022; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | | OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:10349 → `Flatten{}` pine_adapter.cpp:10382-10383; `Reduce{ExplicitUnits}` pine_adapter.cpp:9198-9207; `ReverseTo` pine_adapter.cpp:7668-7669) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes `ReductionSize` native_order.hpp:215, `ReverseTo` native_order.hpp:60-62, `OpeningShape` native_order.hpp:910-914, `Flatten` native_order.hpp:36 | K | — | F:A4 F:A8 O:A2 O:A3 S:O1 | — | | OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2809-4044 → `host.replace` pine_adapter.cpp:3259-3356) | **yes** — `replace` native_host.hpp:1218-1220, `cancel` native_host.hpp:1249; `ReplacedEvent` native_order.hpp:1017-1031; result and event types `SubmitStatus` native_order.hpp:835-861, `ReplacedEvent` native_order.hpp:1001-1067 | K | — | F:A2 O:A4 S:O3 | id → handle map is host bookkeeping (F) → OL4 | | OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:5349-5395, `cancel_all` pine_adapter.cpp:25555-25570; `live_by_source_key_` pine_adapter.hpp:2486; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4595-4669) | **partial** — single-handle `cancel` native_host.hpp:1249; no enumeration (`WorkingRequestCore::live()` is consumer-internal native_order.hpp:1703); labels exist but are unindexed (`Request::label` native_order.hpp:485) **Closed:** `cancel_all` native_host.hpp:1267, `cancel_where(comment)` native_host.hpp:1272 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1272 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1263 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | @@ -57,7 +57,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1104-1105 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | | OL9 | Birth eligibility: an order accepted at bar N cannot fill on N's already-delivered points | **yes** — `point_eligible` native_order.hpp:786-790, the generic birth record `OrderBirth` include/pineforge/order_birth.hpp:59, contract `docs/pages/native-engine.md` ("Acceptance is not a fill") | K+A | — | F:A13 O:A10 S:R4 | **Disagree:** F yes / O partial / S "no, TV quirk". R5-1: the generic core (birth cursor) is native; TV's bar-granular first-bar rule is the A half → OL14. | | OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:10842; lowering `submit_one` pine_adapter.cpp:11699-11711, `BindCohort` pine_adapter.cpp:12161-12168, `submit_leg` pine_adapter.cpp:12248-12272; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11099-11102; `materialize_relative_exits` pine_adapter.cpp:24641-24683, pine_adapter.hpp:2222-2234) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:5902-5936. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:11099-11102; `materialize_relative_exits` pine_adapter.cpp:24641-24683, pine_adapter.hpp:2222-2234) | **partial** — composable: the host computes levels in `on_native_applied` and submits; suffix eligibility makes that same-bar correct `born_on_remaining_path` native_execution_consumer.cpp:5916-5950. `Trigger` itself is absolute-only native_order.hpp:296 **Closed:** `Request::anchor = FromOwnerFill{offset, ticks, rounding}` (`TriggerAnchor` native_order.hpp:336) defers the level to the owner's fill, the kernel materializes it once at the arm under `NativeAnchorRounding`, and `resolve_anchored_level` native_host.hpp:1012 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | | OL12 | Working-order view for the host and outside observers (`strategy_pending_order_*`; `PendingIntentView` pine_adapter.hpp:1449-1465; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:26787-26816) | **no** — base returns 0 / -1 `observe_last_bar_dual_entry_path_v1` engine_consumer.cpp:124-133; the POD is TV-shaped (`tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135) **Closed for a bare host:** `native_working_requests()` native_host.hpp:1263 returns one owning `NativeWorkingRequest` native_host.hpp:681 per live request — definition, remaining, trigger state — with the C spelling `strategy_native_working_len_v1` / `_get_v1`. The TV-shaped POD stays frozen and stays the source layer's projection (`tests/test_native_limit_fill_through.cpp`, `tests/test_native_arm_options.cpp`, `tests/test_native_c_api.c`). | K+A | L7 (C++), L13 (C) | F:J5 O:J6 S:X4 | **Disagree:** O "partial + TV-shaped" / F, S no. A bare host reads zeros → no. R5-1: neutral view → kernel; POD shape and Pine journal retention stay A (POD frozen, §2.ii j). | | OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:2480-2499); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1226-1305); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3896-4063, `materialize_snapshot` pine_adapter.cpp:13733-13757), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1797-1820, `CloseCallsiteState` pine_adapter.hpp:1949-1963; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21386, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21620) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:676-678 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | | OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:44-90, enforced `leg_activation` pine_adapter.cpp:1197-1202; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:986-1019) | n/a | A | — | S:R4 O:A10 | TV half of OL9. **V19D-P1 (R5 lanes B-ADAPTER, V19-FIX):** a margin-call revival never restores an exit the script cancelled: `strategy.cancel` / `cancel_all` retire every leg of each exit row they name — the live ones they take off the book (caused at the Cancelled event, after anything the same callback did to the leg), the dormant ones, and the ones the adapter took off the book itself — and drop a gapped stop a declined reversal parked for the next margin-call slice (`retire_cancelled_exits` pine_adapter.cpp:2061); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2135). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | @@ -66,9 +66,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5872-5894; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5886-5908; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | | TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:44 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:350-352) | **partial** — absolute prices and a raw price distance only **Closed:** `TrailTicks{n}` (`Trail::ticks` native_order.hpp:293) is resolved against `NativeRunSpec::price_tick` at acceptance, so a tick-spelled trail and its price-spelled equal behave identically bar for bar; a relative arm threshold is the `FromOwnerFill` anchor of OL11 (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K | L7 | F:B2 O:B2 S:T2 | **Disagree:** S classes all trail operand conversion as TV quirk. R5-1: tick / relative spelling is generic. | -| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:14774-14786) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4760-4765) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14510-14698) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:14774-14786) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4774-4779) **Closed:** a zero offset is legal and means *ride the best* — the level is the running best itself and the exit is the first print strictly past it; negative and nonfinite offsets stay rejected (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B3 F:B3 S:T2 | **Disagree:** O kernel / F, S quirk. R5-1: accepting a zero offset is generic; TV zero-offset *pricing* (`zero_trail_sibling_stop` pine_adapter.cpp:14510-14698) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4574-4622) | **partial** — the read exists `trail_state` native_host.hpp:1092; `replace` always rebuilds trigger state **Closed:** `replace(handle, request, ReplaceOptions{/*retain_trigger_state=*/true})` carries the predecessor's live trigger state — a tracking trail's best, an already active stop — into the successor, refusing the replacement when the retained best leaves no representable level (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | O:B4 F:B3 | Status is single-source (O). R5-1: retain-trail replace is generic; TV's "retained best across re-issue" convention (`retained_trail_best` pine_adapter.hpp:353-355) is A. | | TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:12398-12403, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14510-14698, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:10450-10966, `trail_one_shot` pine_adapter.cpp:12206-12295 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -77,12 +77,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7049-7053, pine_adapter.cpp:7053) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | -| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:13726-14954) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5249, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5215-5249 | K | — | F:C2 O:H9 S:S2 | **Disagree:** S "partial" (a hook is not a basis). The hook is native; bases are SZ3-SZ5. It stays the custom-policy seam after L3. | -| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:3030-3094) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5278-5280 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:3024 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, `tests/fixtures/margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:606 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:13726-14954) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5263, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5229-5263 | K | — | F:C2 O:H9 S:S2 | **Disagree:** S "partial" (a hook is not a basis). The hook is native; bases are SZ3-SZ5. It stays the custom-policy seam after L3. | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:3030-3094) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5292-5294 **Closed:** `Sized{Side, EquityFraction{f}}` (`OrderIntent` native_order.hpp:229) is the sixth intent — the kernel resolves the units from a fraction of marked equity at the sizing point, so a bare host needs no terms override (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:C3 O:C2 S:S3 | TV parts stay A: the percentage's money rounded `source_money_round` pine_adapter.cpp:3024 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, `tests/fixtures/margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:606 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2996-3005) | **no** — same **Closed:** `Sized{Side, CashValue{cash}}`, the same resolution over an account-currency amount (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:C2 O:C3 S:S4 | A per-call `qty_type` (cash or percent of equity on one `strategy.entry`) is not Pine — TradingView's compiler refuses it, so only generated or C++ callers reach it — and since R5 lane B-ADAPTER it converts through the kernel's `native_sized_units` exactly as the declared default does (`typed_quantity_units` pine_adapter.cpp:3100, `typed_entry_units` pine_adapter.cpp:3118; `tests/test_adapter_typed_entry_admission.cpp`) | | SZ5 | Fee-net sizing (commission reserve `/(1+c)`, asked of the kernel as `reserve_percent_fee` pine_adapter.cpp:3079; `percent_commission_live_equity` pine_adapter.cpp:4899-4954) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in `tests/fixtures/cash_fee_sizing`) | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2942-2960, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8066-8120) | **no** **Closed:** `SizeTime::AtMatch` (default) resolves at the matching candidate and `SizeTime::AtAcceptance` freezes the units against the command point, where the run's own opening admission also runs against the frozen quantity (`RequestRejectReason::PlacementAdmission`). Which price the basis converts at is `SizePrice::{Resolved, Signal, SignalOnTick}` (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K+A | L3 | F:C3 O:C7 S:S3 | **Disagree:** O adapter-only / F, S generic timing knob. R5-11 (L3 scope) + R5-1: the knob is kernel; TV's frozen tuple stays A through `HostSized`. | -| SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:660, native_order.hpp:1421-1438; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:5295-5298; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | +| SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:660, native_order.hpp:1421-1438; host-sized terms snap (`ExecutionGridPolicy` native_order.hpp:71-85); exact grid validation `execution_terms_grid_representable` native_execution_consumer.cpp:5309-5312; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:15482-15510, `compute_exit_reservation` pine_adapter.cpp:4988-5117) | **no** — sizes are `ExplicitUnits` / `OwnerOpenedUnits` / `HostSized` only native_order.hpp:60-74 **Closed:** `Reduce{ScopeFraction{fraction, claim, basis}}` takes a fraction in `(0, 1]` of the scope the reduce is bound to — the book, one opening, or a cohort roster (`tests/test_native_sizing_bases.cpp`). | K | L3 | F:A7 O:A12 O:C4 S:S5 | F's "partial" counts the units-reduce of OL2. R5-4 → L3. | | SZ9 | Exit-quantity reservation against sibling exits (`compute_exit_reservation` pine_adapter.cpp:4747-5111, `reserved_exit_qty` pine_adapter.cpp:10886-11362) | **partial** — `PointBudget` native_order.hpp:379-382 and group `Reduce` native_order.hpp:220 give a generic reservation; no net-of-siblings claim for fraction sizes **Closed:** `ScopeClaim::NetOfSiblings` subtracts the units already claimed by live sibling reduces bound to the same scope, and `ScopeBasis::{AtMatch, AtAcceptance}` chooses when that scope is measured (`tests/test_native_sizing_bases.cpp`). | K+A | L3 | F:A14 S:S5 S:O10 O:A12 | F proposed nothing. R5-4: generic half → L3; the Pine ledger → SZ11, SZ12. | | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:499-506, `source_money_floor_lot` pine_adapter.cpp:606; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5075-5090) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | @@ -94,12 +94,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2794) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3245-3253 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | -| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3259-3262 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16267-16270) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3264-3267 | K+A | — | F:A5 O:A13 O:D6 S:M1 | **Disagree:** F, O:A13 partial / O:D6, S yes. R5-1: Pine's per-cycle entry count (`accepted_in_cycle` pine_adapter.cpp:7504-7525, cap left unset `max_open_lots` pine_adapter.cpp:2792-2794) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: `tests/fixtures/pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11346-11385). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:16143-16356 and leaves the kernel gate unset pine_adapter.cpp:16352-16354) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3282 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3128 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (`tests/test_explicit_market_admission_tapes.cpp`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2794) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3259-3267 | K | — | F:D4 O:D7 S:M1 | the one risk rule the adapter delegates (O) | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:663 tests the resulting book native_execution_consumer.cpp:3273-3276 | K+A | — | F:D5 O:D5 S:M1 S:M5 | **Disagree:** O, S:M5 partial. R5-1: the generic cap is native; the adapter's live-position `>= held` variant (`max_position_size` pine_adapter.cpp:16267-16270) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3278-3281 | K+A | — | F:A5 O:A13 O:D6 S:M1 | **Disagree:** F, O:A13 partial / O:D6, S yes. R5-1: Pine's per-cycle entry count (`accepted_in_cycle` pine_adapter.cpp:7504-7525, cap left unset `max_open_lots` pine_adapter.cpp:2792-2794) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 13 (it keeps a slot a close or another entry's exit drained; since lane W5B-ENG-MARGIN-RESIDUAL's SB the later market entries of a flat bar are refused at the fill once the book holds the cap). The fifteenth, a resting entry accepted below the cap and filled at it, the kernel's cap refuses; the adapter books it since lane W8A-SIGSTATE-1's R-B, which also stopped the count from counting a resting LIMIT entry (a pending LIMIT entry does not count at a later call, and one the cap admitted fills over it: `tests/fixtures/pyramiding_open_order`). A recorded divergence, retained pending a re-lowering ruling. **Re-lowering tried by R5 lane H-THIN and not taken:** on the kernel's cap four corpus probes moved away from TradingView, for want of that first-eligible-point rule, so the count stays. The lane fixed the reservation divergence H-MEASURE measured beside it (P10): a default `strategy.exit(from_entry)` under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves the entry's own quantity, the oldest lots first (`fifo_named_entry` pine_adapter.cpp:11346-11385). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:16143-16356 and leaves the kernel gate unset pine_adapter.cpp:16352-16354) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3296 against marked equity. **Conversion instant, R5 follow-up lane E21:** both sites of the gate convert at their own point's rate — the candidate at its cursor, the placement gate at the acceptance point where a `Sized{SizeTime::AtAcceptance}` freezes its units — for the notional, the equity and a percent ticket, and the freeze's equity basis with them. The rate is threaded, never the clock: `make_command_context` native_execution_consumer.cpp:3142 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). **Since R5 lane TAIL-C** an explicit-quantity market buy from flat is costed at its signal close's tick plus the slippage ticks its fill carries, off `process_orders_on_close` (`tests/test_explicit_market_admission_tapes.cpp`), and an infinite `strategy.entry` quantity trades the default quantity, as na does (`tests/test_nonfinite_entry_qty_tape.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:17092-17093, `margin_pct` pine_adapter.cpp:16534) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:666 **Closed:** `NativeMarginModel::initial_long` / `initial_short` native_run_spec.hpp:242, opt-in and mutually exclusive with `initial_margin_fraction`; `initial_ == 0.0` is the maintenance-only spelling that leaves opening admission to the host at both of its gates (`tests/test_native_margin_model.cpp`). | K | L4 | F:D1 O:D2 | R5-1 | -| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17076-17359, `submit_margin_call_units` pine_adapter.cpp:18396-18444, `schedule_margin_call_path` pine_adapter.cpp:25425-25541) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3241 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:17076-17359, `submit_margin_call_units` pine_adapter.cpp:18396-18444, `schedule_margin_call_path` pine_adapter.cpp:25425-25541) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:666-667; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:3255 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | | MG7 | Liquidation slice sizing (grid floor, ×4 restore, one-contract fallback `source_margin_units` pine_adapter.cpp:17303-17374) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). A four-times call on a whole-unit grid whose book's oldest lot is one unit is followed by one more unit at the next check point (`whole_unit_follow_up_due` pine_adapter.cpp:18900; R5 lane W5B-ENG-MARGIN-RESIDUAL, `tests/fixtures/margin_residual` w5b-es-*, w5b-sb-es-*, w5b-f-q1x12). With slippage the one-contract band takes its unit only where that unit restores the book at its own fill: the position's requirement at the liquidation's slipped print, less the equity at the mark, under one unit's margin (`source_margin_units` pine_adapter.cpp:17372; lane INT28-FIX rule OU, `tests/fixtures/slipped_short`). A one-unit call that leaves the book short at its own fill, by more than one unit's margin and two slippage steps with the book re-marked there, is followed by one more unit at the bar's next path point, where that point's own check calls nothing and its print is inside the call's fill (lane INT28-FIX rule OF). Under a percent commission a call of any size is followed so, on a fractional lot grid too: while the book the call left, re-marked at its fill, is short by more than two slippage steps for each unit called and the exit fees of those units and of one more, the next path point inside that fill takes four times the shortfall's lot floor over the fill, or one unit where that floors to none (`margin_follow_up_units` pine_adapter.cpp:18942, `follow_margin_call` pine_adapter.cpp:18991; R5 lane TAIL-I, `tests/fixtures/margin_open_print`). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:168-184) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1320 answers the exact solved level, `nullopt` where none exists; `NativeLiquidationLevelBase` chooses whether the solve answers for the open entries' fees (`tests/test_native_margin_model.cpp`, `tests/test_native_open_lots.cpp`). | K+A | L4 | F:D3 S:R5 | **Disagree:** F generic / S adapter-only. R5-1: liquidation price is generic; the Pine spelling stays A. **Lowered by R5 lane H-THIN:** the Pine host's second solve (`compute_liquidation_price`) is gone; `strategy.margin_liquidation_price` reads `native_liquidation_price()`, which the adapter configures with `NativeLiquidationLevelBase::RealizedOnly` (the Pine formula's closed-money solve, bit for bit at every bar of the lane's 1,021 seeded margin-call runs), and keeps only TradingView's tick spelling: floored for a long, ceiled for a short. A run whose margin calls are switched off declares no margin model, so it reads na. | | MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:22182-22258, `apply_fx_opening_margin_slice` pine_adapter.cpp:25587-25615) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1202 but no margin re-check. **Closed by audit lane N6:** `NativeMarginCheckKind::FxRoll` — a step of the declared curve is a check point of its own, measured at the unchanged price before the first point the new rate converts (`tests/test_native_margin_fx_roll.cpp`). The adapter refuses the point by kind, for a measured reason since R5 lane F7: TradingView's rollover is a broker-open checkpoint at the open price, now at any positive margin (`apply_fx_open_margin_slice`, on the one TradingView money / units rule every other checkpoint uses), while the roll point measures at the remaining path's adverse mark and rests the slice there — a carried 1x long on a step bar O 100 L 97 books 0.4116 @ 97 on the adapter's run shape and 0.4036 @ 99, measured a bar early, on a strict calendar, against TradingView's 0.3996 @ 100 (`tests/test_native_margin_hooks_twin.cpp` MG-FX). Both N6 follow-ups are closed (§4.4). **Completed by R5 follow-up lane E3:** the conversion INSTANT is now generic too — every check kind converts at its own cursor's rate (`account_currency_fx_at(cursor.point.effective_time_ms)`) for the requirement, the equity and the level, in batch and stream, so a step landing between a fill's cursor and the clock the engine presents no longer breaches at the fill (`tests/test_native_margin_fx_clock.cpp`; N6's local clock swap inside `fx_roll_margin_check` is removed as redundant). **Extended by R5 follow-up lane E21** to the opening gate's placement site (MG4), and the rate-explicit equity E3 restated beside the engine's accessor is now that accessor's one implementation, `BacktestEngine::marked_equity_at` engine.hpp:729 | K | L4 → N6 → E3 → E21 | O:D8 | (single-source) | @@ -115,15 +115,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7519-7552 (call site `on_native_recalculate` native_execution_consumer.cpp:7571) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2750-2751) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7982-7986 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22011) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7416-7420; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | -| CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7363 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7414 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | +| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7533-7566 (call site `on_native_recalculate` native_execution_consumer.cpp:7585) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2750-2751) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7996-8000 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22011) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7434-7438; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT4 | Partial-bar view / open-bar lookahead guard | **no** — the open hook receives the *complete* script bar: `invoke_bar_open_callback(engine, bar, point)` native_execution_consumer.cpp:7377 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7428 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | | CT5 | Raw input observation before aggregation | **yes** — `on_native_input` native_host.hpp:875, `NativeInputContext` native_host.hpp:773-778 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:671, `coof_recalculation_due` pine_scheduler_native.cpp:637, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:792; `begin_coof_recalc` pine_adapter.cpp:6535, `flush_coof_tail` pine_adapter.cpp:22235, `end_coof_recalc` pine_adapter.cpp:6592) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7267, native_execution_consumer.cpp:7267-7297; commands are legal there `commands_allowed` native_execution_consumer.cpp:1500-1508; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5902-5936, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7498-7502; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:277-279. | -| CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:7933-8075 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | -| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2814-2870) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7718-7984; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:671, `coof_recalculation_due` pine_scheduler_native.cpp:637, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:792; `begin_coof_recalc` pine_adapter.cpp:6535, `flush_coof_tail` pine_adapter.cpp:22235, `end_coof_recalc` pine_adapter.cpp:6592) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7281, native_execution_consumer.cpp:7267-7297; commands are legal there `commands_allowed` native_execution_consumer.cpp:1512-1520; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5916-5950, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7516-7520; no per-tick calculation in batch; the guide pinned "close-only" (`docs/pages/native-engine.md`, "Batch OHLCV vs ticks vs quiet") **Closed:** `NativeCalculationTrigger::EveryModeledPoint` native_run_spec.hpp:97 adds one `Tick` recalculation at every modeled point of the delivered path and at every observed print, in batch and in a stream alike (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E5 O:F4 S:C3 | **Disagree:** F yes / O partial / S no. R5-5: v1. Not an adapter feature either: absent from `PineStrategyConfig` pine_adapter.hpp:62-76, the runner rejects it (`calc_on_every_tick` main.cpp:259), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:277-279. | +| CT8 | Historical intrabar calculation (per lower-TF sub-bar / magnifier sample) | **no** — with an `IntrabarPath` the callback still fires once per script bar native_execution_consumer.cpp:7947-8089 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:928 fires once after each retained lower-timeframe sub-bar's whole matching path (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E6 O:F8 S:C4 | — | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2814-2870) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7732-7998; `NativePathOrder` native_run_spec.hpp:335-339 | K+A | — | F:E7 F:G5 O:F7 S:C4 | **Disagree:** S partial (it wants callback cadence → CT8). TV sampler choice is A: TradingView's own intrabars at its table's timeframe, each owned by the chart bar of its last minute, built from the finer feed (`tradingview_magnifier_bars` magnifier_intrabars.hpp:44, R5 lane MAG-INTRABAR). | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1771, `stream_push_bar` engine.hpp:1777, `stream_end` engine.hpp:1781, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:658 | K+A | — | F:E8 O:J3 S:C5 | **Disagree:** S partial. R5-1: the lifecycle is native; Pine trade-start suppression and realtime-tail / probe rules (`trading_window_active` pine_strategy_commands.cpp:15-21, `prepare_native_begin` pine_strategy_host.cpp:206-307) are A. S's "neutral tail + trade-window policy" is single-source and unassigned (§4 Q8). | | CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:6808), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:54-106; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:792, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1906-1936 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,37 +132,37 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1474-1477, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1529-1828; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1475 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:491), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:887) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1523-1539 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1606; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1556-1561 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8824 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133, C ABI `strategy_set_native_security_feed` c_abi.cpp:887) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1535-1551 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1606; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1568-1573 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:493 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8838 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:51-61). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:808) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:23-241, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:262-488; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:524) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. **Closed (lane INT28-FIX, rule RC):** a `calc_on_order_fills` recalculation reads the calling bar's own slice, as the bar's close calculation does and as TradingView's does after the open fill or a later fill (a lookahead_on site through the slice's first bucket; `lab tv` tapes `tests/fixtures/coof_recalc_security`): the bar's first recalculation before its close callback feeds it (`scheduler_feed_aux_security` pine_scheduler_native.cpp:716). The close callback fed it alone (`scheduler_feed_aux_security` pine_scheduler_native.cpp:578, now only when no recalculation did), after the bar's recalculations had read the previous bar's; a recalculation after that callback feeds nothing. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:282-397) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:8366-8383, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8384, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:8391-8408, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8398, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5837-5884, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6579-6587; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6582-6592) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5260-5278) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:4911-4912, clamped by `protect_limit` native_execution_consumer.cpp:6582 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:14938-14947) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5837-5884, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6593-6601; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6596-6606) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5274-5292) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:652 applied once native_execution_consumer.cpp:4925-4926, clamped by `protect_limit` native_execution_consumer.cpp:6596 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:14938-14947) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:616-623) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:249-252, `source_bar_fill` pine_adapter.cpp:14938-14947, `directional_tick` pine_adapter.cpp:294-298) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4373; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6668-6669; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:249-252, `source_bar_fill` pine_adapter.cpp:14938-14947, `directional_tick` pine_adapter.cpp:294-298) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4387; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6682-6683; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:325-373) is A on top. | | PG | The Pine adapter on the kernel price grid: raw levels submitted, `project()` declaring `QuantizeFillsAndTriggers` + `HalfUp`, `source_trigger_threshold` deleted (lane R7; re-tested after L8b by gap lane N13) | **measured-infeasible for the Pine adapter** — B1 (the sub-tick cursor-print abort) is closed by L8b's `ActivationGrid`: 0 aborted runs under the trial (R7: 9 zero-offset-trail tapes + 6 POOC panels; now `test_zero_offset_trail_rides_l4c` 449/450, `test_pooc_short_close_tick_l4d` 183/183). B2 (TradingView quantizes per order kind, the grid is one run-wide rule) moved 30 pinned checks in 4 units when N13 last ran the trial, before lanes E14 and E16, with no adapter-side remedy: `test_coof_market_limit_recross_l4c` 24, `test_stop_tick_rounding_l4d` 3, `test_adapter_grid_relower` 2, `test_zero_offset_trail_rides_l4c` 1; 12 more checks in 3 units have an adapter-side cause (`test_trail_fill_snap_l4c` 2, `test_native_margin_hooks` 2, `test_adapter_brackets_relower` 8 book digests). Corpus: 5/312 probes differ, in the engine-only entry-incarnation column only (every probe runs a 0.01 tick on an on-grid feed, so it cannot arbitrate) | A | waived (R7, N13) | — | **Waiver.** No class of triggers is byte-identical on its own: the grid is a run-wide switch and a per-kind mask would spell TradingView's inconsistency into the kernel (not generic, R5-3). The adapter stays on `NativePriceGrid::None` and keeps `source_trigger_threshold` and its call sites; native-engine.md "The Pine adapter's grid re-lowering is waived". **Native-only by ruling (audit lane P6): §3.6.2**, with `examples/native/native_price_grid_strategy.cpp` and its C twin as the feature's hosts. | -| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9268). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:9282). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | | FP7 | *(quirk)* TV tick conventions: half-tick threshold `source_bar_fill_tick` pine_adapter.cpp:332-380 (used `source_trigger_threshold` pine_adapter.cpp:11518-11523, `exit_limit_trigger` pine_adapter.cpp:12248-12269), `source_bar_fill_tick` pine_adapter.cpp:259-272, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:288-292, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17535-17541 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14623) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2900-2913, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is tape `tests/fixtures/e19_allin_trim`. | -| RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:38-65, `get_trade` engine.hpp:1792-1829, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2914-2927, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is tape `tests/fixtures/e19_allin_trim`. | +| RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-85, `get_trade` engine.hpp:1792-1829, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | -| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:118-125 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | +| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:138-145 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1932) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1831 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1399) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1849 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9940, C ABI `strategy_stream_order_actions_len` c_abi.cpp:697-714 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:11455; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9958, C ABI `strategy_stream_order_actions_len` c_abi.cpp:697-714 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:11473; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:572; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2444 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, 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. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -176,7 +176,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2174, `NativeAbortReporting` native_run_spec.hpp:41-44 | K | — | F:J4 | (single-source) | | OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:930, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1001, probe `strategy_execution_contract` c_abi.cpp:921, stream `strategy_stream_begin` c_abi.cpp:658 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 73 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:267-302, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7622), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2167 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1138; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:267-302, `chart_index` pine_scheduler_native.cpp:570-703) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1485 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7636), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2167 selects the kernel's facts into the flags, and only widens the plain `session.isfirstbar` / `islastbar` pair to the chart's day on an extended-hours chart (R5 lane K-SESSION-WINDOWS F2: TradingView's `isfirstbar` / `islastbar` span the pre- and post-market bars there while the `_regular` pair keeps the regular day, `tests/test_session_extended_day_tape.cpp`), which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:1138; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:282-284) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -204,9 +204,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---| | a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:83-92) and of the rich `BacktestEngine::run` overload (engine.hpp:1796-1801, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:758; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:285-290 | NEUTRAL in behaviour; a consumer-vtable signature change, so it rides the shared `engine_script_run` bump | `const void*` in the kernel signatures (F `host_begin_extras`, O `overrides_opaque`), typed convenience overload in `source/pine_strategy_host.hpp`; then drop the checker's single whitelist entry `test_native_example_batch` check_native_include_independence.py:42-46. S's alternative (`NativeRunOverrides`, or removing the rich overload) not adopted. Unverified: generated code calls the typed overload (codegen repo is not in this tree — F). **Status (L11, `3f4fff5b`): done** — `run_rich` and the rich `BacktestEngine::run` take `const void*` overrides, the rich begin carries them as `overrides_opaque`, and the independence checker whitelists nothing (§2.vi row 6) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:30 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:337-340 (DMI); no execution TU includes it (O) | NEUTRAL (rename only) | Rename to a neutral header (F `float_band_compare.hpp`, O `ta_compare_band.hpp`), keep `pine_float_*` inline aliases in a source-layer header for generated code. Numerics must not move. S's behavioural `ComparePolicy` split is not neutral → not adopted (§4 U6). **Status (L11 + N14): done** — `ta_compare_band.hpp` is the kernel header; the alias shim is `include/pineforge/source/pine_float_compare.hpp` (N14 moved it off the kernel include root; it has no in-tree includer, generated code emits its own comparator) | F O S | -| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:951-953 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1973-1976; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1990, `:9398`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2740-2741. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:269-270. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | +| c | `NativeSlotLabelPolicy::LegacyTolerant`, `NativeLegacyTolerance` (now `NativeSlotLabelPolicy::FeedTolerant`, `NativeFeedTolerance`) | `NativeSlotLabelPolicy` native_run_spec.hpp:324-353; read by `legacy_tolerant_slot_labels` market_driver.cpp:55-56 and `native_feed_tolerance_enabled` market_driver.cpp:43-49, and by `legacy_tolerant_slot_labels` native_execution_consumer.hpp:960-962 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1985-1988; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2002, `:9412`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2740-2741. A feed-tolerance policy (raw strictly-increasing labels, zero / NaN-volume legacy bars), not TV semantics (O) | NEUTRAL | Rename with deprecated aliases (F `RawIncreasing` + `NativeInputTolerance::{StructuralBars, WarmupNonNegativeOHLC}`; O `NativeLabelPolicy::ProviderLabels`). Enumerator values stay 0/1 and 1/2: they are hashed `slot_label_policy` native_execution_consumer.cpp:269-270. **Status:** renamed as `FeedTolerant` / `NativeFeedTolerance` (the enumerator values unchanged), and the C transport carries them now, in the extension's feed-policy block (`PF_NATIVE_SPEC_EXT_FEED_POLICY` native_c_api.h:745) | F O S | | d | Orphaned comments | The KI-62 block of `engine_orders.cpp` (*fresh*: comment-only, the bodies are deleted; S read it as live code) and a comment of `engine.hpp`; wider deleted-declaration blocks in `engine.hpp`; the money-rule essay in `engine.hpp` (the code is `source_money_round` pine_adapter.cpp:505-511) | NEUTRAL | Delete, or move to `docs/` next to the adapter code that implements them. **Status: done** — R5 lanes E6 and E10 deleted the detached comment residue (a census of 0 over the kernel's compile closure) | F O | -| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6738 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6752 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | | f | Dead legacy stream path | **Done.** `stream_feed_input_bar` and `dispatch_source_stream_script_bar` are both gone — neither name is in `src/` or `include/` — and the virtual left with the epoch that carried it. `stream_state_hash` engine.hpp:1790 stays, and the declaration/definition pairing is still enforced by `scripts/test_native_source_guard.py` | NEUTRAL (removing the virtual changes the vtable → shared bump) | Delete both | O; F asked (its Q6) | | g | Legacy path resolver | **Done, and then deleted.** `src/engine_path_resolve.cpp` is 118 lines with five functions: `bar_path_uses_high_first`, both `first_touch_position` overloads, `fill_bar_path_points_ordered`, and `BacktestEngine::declare_opened_lot_entry_bar_mask`. Ten test files still mention `resolve_exit_path_fill` in historical comments; the resolver body and test-only oracle are gone. | NEUTRAL (pure move; verify with the link gate) | Keep the generic path helpers in the kernel TU; the old source resolver is not a live counterpart | F (single-source) | | | h | `pine_*`-named generic API | `timeframe_time` session_time.hpp:35-211 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:263), `str_utils.hpp`, `deterministic_random` math.hpp:9, `tz_util` timezone.hpp:8-30; the guard bans `pine_*` only inside `IDENTIFIER_ROOTS` (test_native_source_guard.py:34, test_native_source_guard.py:34-42) | NEUTRAL, M | Neutral aliases; `pine_*` kept as deprecated inlines for codegen. Large blast radius in generated code. **Status (L11 + N14): done** — session_time / str_utils / math / timezone in L11; N14 applied the same treatment to the two public matrix types: `PineMatrix` → `NumericMatrix`, `PineGenericMatrix` → `GenericMatrix`, old spellings kept as exact aliases (no version script pins either name) | O (single-source) | @@ -267,8 +267,8 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | -| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7578 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1500-1507): inside a callback, or between realtime inputs | F | +| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7592 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1512-1519): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or the `source-guard-c-abi` stage fails, in preflight and every `ci_verify` profile (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:678-693, `contribute_input` native_execution_consumer.cpp:8513, `seal_script` native_execution_consumer.cpp:8478) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7452), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:678-693, `contribute_input` native_execution_consumer.cpp:8527, `seal_script` native_execution_consumer.cpp:8492) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7466), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open then → §4 U1; landed as `lookahead` and `gaps`, both default false (HT5) | @@ -290,7 +290,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1731, engine_aux_security.cpp:79-133); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:136, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. 3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1593-1677 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:932, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). -5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:83, which latches `UnsupportedSource` native_execution_consumer.cpp:1556); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. +5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:83, which latches `UnsupportedSource` native_execution_consumer.cpp:1568); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). 7. **HT3 / HT4:** lower-TF arrays come from the L5 sub-bar hook in v1. An auxiliary feed is a subscription whose bars come from the host; the Pine chart-slice mapping is not exposed. Whether v1 accepts a feed *finer* than the input is left to the lane brief (S wants it, F calls it a TV harness construct). **Status (audit lane N7): HT4 done for a bare host; the adapter's auxiliary path retained, measured.** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed{tf, bars}`, strictly finer than `input_tf`, opt-in and hashed only when present) plus `NativeTimeframeSubscription::source = NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars through the existing evaluator step (`register_security_eval` against the feed's timeframe, `feed_security_eval_state` per feed bar — called, not edited); routing is by time alone (a feed bar rides on the first accepted input whose period it opened before), with `declare_auxiliary_feed` at begin, `append_auxiliary_bars` on a realtime stream and a C spelling (`PF_NATIVE_SPEC_EXT_AUXILIARY_FEED` tail, `strategy_native_append_auxiliary_bars_v1`). No epoch moved: `native_run_spec_v3` and `engine_script_run_v18` were open then (no release tag contained them, no frozen provider fixture of either existed), exactly as L6c's `gaps` and begin-time hook were added. `BacktestEngine::set_aux_security_feed` stays the source host's door and still answers a silent false on a bare host (the pre-begin setter pins, `tests/test_native_example_batch.cpp` among them, hold it to that). **Retained, why:** the adapter does not re-lower its auxiliary sites, on three measurements — 0 of 312 corpus probes install an auxiliary feed, so corpus byte-identity cannot witness the move; TradingView's chart slice leaves pre-range feed coverage inert where the kernel folds it by time (two hourly buckets against three over one feed, `tests/test_native_auxiliary_feed_twin.cpp` row B); and the adapter evaluates a chart bar's slice at its calculation, after that bar's matching pass, where the kernel delivers before it (position 1 against 0 at the same bucket, row C), which no predicate over an opaque generated `evaluate_security` can prove neutral. The congruent shape is the same series on both (row A) and can move once a population exercises it. Full contract: `docs/pages/native-engine.md`, "The auxiliary finer feed". @@ -343,7 +343,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:19 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:289 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9761-9803 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9810-9852 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -352,7 +352,7 @@ the campaign's own anchor audit found that the first thing to rot: ### 3.1 Parity discipline for every lane (R5-10) — stated once, referenced per lane -- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2524-2791) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1788-1792). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. +- **(a) NEUTRALITY.** Every new behaviour is opt-in by a spec field or a new request kind. The adapter's `project()` (pine_adapter.cpp:2524-2791) never sets the field and never emits the kind, so adapter runs are byte-identical by construction. The four feed-shape fields are folded into `hash_spec` unconditionally today, including defaults (native_execution_consumer.cpp:238-260), so even a defaulted new field would move every continuation hash; the precedent for a conditional digest is `precommit_digest_` (native_execution_consumer.hpp:1812-1816). Each lane adds a test that a default spec hashes to the pre-change constant. Every new durable field is hashed (`check_broker_state_hash_coverage.py`, fail-closed, waivers in `broker_state_hash_waivers.txt`). Proof per PR: `scripts/run_corpus.sh` + `scripts/verify_corpus.py` locally, then ONE campaign sweep on the final tree, run by the maintainers (fixed population, 4190 probes when this plan was written, zero tolerance on the hard band — native-refactor-progress.md:42-48; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:144-145). A lane that cannot be shown neutral by construction does not merge. - **(b) TWIN.** One chosen probe per lane runs through the adapter AND a hand-written `NativeStrategyHost` on the same bars and spec. Trade lists are diffed **by identity** (entry / exit time, price, qty, pnl, commission), never by trade number; then event, ownership and hash detail (S's D → E order). A difference is allowed only where a named TV-quirk row of §1 is active, and each one is itemized. Because 430 test files drove the adapter and 14 the native host when this plan was written (O; on this tree 543 `tests/*.cpp` name `source::PineStrategyHost` and 110 name `NativeStrategyHost`), every lane also adds native-only tests: a lane is not accepted on the corpus alone, nor on a unit test alone (S). - **(c) EPOCH BUDGET.** Three inline-namespace epochs move — the request epoch, the host epoch and (*fresh*, not in the inputs) the run-spec epoch, which every new spec field moves. They stood at `native_order_v5`, `engine_script_run_v17` and `native_run_spec_v2` when this plan was written; on this tree they are `native_order_v7` (native_order.hpp:25), `engine_script_run_v19` (native_host.hpp:20) and `native_run_spec_v3` (native_run_spec.hpp:15): the budget below was spent exactly once, and the v19 value epoch (R5 lane V19-A) took the host epoch one step past it. One bump each per release batch: `native_order_v6` = L3 + L7 + L4's origin / event; `engine_script_run_v18` = L4's host hooks + L5 + L6 virtuals + L2's `hash_host_extension` + §2.ii a, f; `native_run_spec_v3` = the spec blocks of L2, L4, L5, L6, L8 (L9 if it ships in the batch). A lane that lands before its batch closes stages behind the open epoch, so codegen-built strategies rebuild once per batch. Epoch lanes are not neutral-refactor lanes: the brief lists the header extension explicitly; the frozen C++ ABI fixtures (`tests/fixtures/native_cpp_abi/host-*`) and the ABI checkers (`check_native_cpp_abi.py`, `check_settlement_cpp_abi.py`) move with them; `variant_size` pins change (`OrderIntent` native_order.hpp:229, `CommandEvent` native_order.hpp:1308). Every new test unit compiles first against the frozen previous-epoch header closure (fail-before, `CLAUDE.md`). - **Worker verification** (`CLAUDE.md`): `ci_preflight.py`, then `ci_verify.py release`; plus `check_c_abi_runtime.py` and `check_native_include_independence.py`. No campaign sweep from a worker. @@ -413,7 +413,7 @@ closure markers and `scripts/check_design_inventory.py`). | L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2790, so the kernel records the same series at the adapter's own report marks — still one recorder, no double recording, `fill_report` bytes unchanged on the corpus | native example: curve length == script bars, drawdown equals a hand walk (F); 50-bar host asserts a non-empty curve, a finite max drawdown and a range-end row (O); same generic strategy through both hosts → equal rows and curve (S) | | L3 | `Sized{side, basis, time, grid_policy, reserve_percent_fee}` as the 6th `OrderIntent`; `ScopeFraction{fraction, claim}` in `ReductionSize` → A.2 | at L3 the adapter emitted neither. Since R2 it lowers a default cash / percent opening frozen at the signal onto `Sized{CashValue, AtAcceptance, SignalOnTick, ExplicitUnits}`, since N11 its placement-time quantity is the kernel's `native_sized_units`, and since R5 lane F7 a typed quantity (`qty_type` cash / percent) converts through the same query and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (`tests/test_adapter_sizing_relower.cpp` cases 1-4, 8, 11, 12). It still never emits `ScopeFraction` (case 5) | percent-of-equity probe, zero fee, no grid: exact; with grid / fee: differences explained by SZ10 only (F). Differential: `Sized` vs a host-resolved `HostSized` on the same facts → identical `ExecutionAppliedEvent` (O). Gap, reversal, fee-bearing first lot; two 50 % siblings, partial close, pending parent, replacement (S) | | L4 | `NativeMarginModel`, `native_liquidation_price()`, `resolve_margin_call_units`, `on_native_margin_call`, `RequestDefinition::origin`, `MarginCallEvent` → A.3 | at L4 the adapter left `margin` unset. Since R5 `project()` declares a maintenance-only model (per-side `initial_*` 0, `maintenance_*` = `margin_long` / `margin_short`, `PathAdverseExtremeMark`, TradingView's equity basis) and answers its three hooks with TradingView's scheduling, money and slice (MG6, MG7), while every opening is still `AdmitWithHostMargin` (MG4) | first a plain maintenance breach in both hosts (S); then a margin-call probe reproduced with `ShortfallMultiple 4`: same bar, same side; quantity deltas itemized against MG15 (F, O) | -| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5930-5934) and `drain_applied_notifications` (native_execution_consumer.cpp:6595-6608) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5944-5948) and `drain_applied_notifications` (native_execution_consumer.cpp:6609-6622) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1086-1170 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | @@ -446,7 +446,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:5278-5280). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:5292-5294). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -518,7 +518,7 @@ adapter's own rule acts. **Per rule (STEP 1), assuming the values were known at begin time.** Adapter line numbers are re-anchored to this tree (the measurement ran on 683a82f); kernel semantics are -`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:3983 +`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:3997 (evaluated at the script bar's open, its close calculation and after each applied drain; fills counted at the corresponding adapter checkpoints; the block refuses `would_open`). | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | @@ -1599,10 +1599,10 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| -| E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1788); test that a default spec hashes to the pre-change constant | F | +| E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp:1812); test that a default spec hashes to the pre-change constant | F | | E2 | **Margin.** The TV margin call is built host-side from kernel primitives (a Stop-triggered `Reduce` bound to openings, or `execute_current`, which survives in `submit_margin_call_slice` pine_adapter.cpp:17715); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:19993 and its excursion sample differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN: the kernel's sampler books it) | A kernel model under an adapter run would liquidate twice or reorder settlement; a checkpoint that reduces mid-path changes lot state exactly where the adapter's chronology lives | Superseded by R5: the adapter declares `spec.margin` — maintenance-only, so no opening is decided twice (`AdmitWithHostMargin`, MG4) — and the kernel owns the mechanism (check points, resting liquidation, receipt) while the adapter's hooks answer TradingView's scheduling, money and slice (ADR-0001 "Margin"). What stays host-side is named there: the chronology exceptions, the pre-open slice, the 1x-long money call and the broker-open FX rollover, whose kernel `FxRoll` point measures elsewhere (MG9). Corrected by R5 lane F7 | F O S | | E3 | **Sizing.** TV sizes from the tick-rounded signal close, 10-digit-rounded equity and fee-reserved cash, frozen at signal, with fill-time exceptions in COOF paths (`default_sizing_shape` pine_adapter.cpp:3070, the COOF `at_fill` exception pine_adapter.cpp:8217) | One "generic" percent sizing serving both would break parity or import TV rounding into the kernel; the last ulp differs on lot-stepped instruments | Two paths on purpose: `Sized` (kernel, plain arithmetic, timing as a first-class field) vs `HostSized` + the adapter resolver. Never "simplify" the adapter onto the kernel basis. What the two paths share is the conversion: since R5 lanes N11 and F7 the division by price × point value × FX and the percentage fee reserve exist once, in the kernel (`native_sized_units`), for a default AND a typed (`qty_type` cash / percent) quantity. The adapter keeps what is measured TradingView: the money basis (signal-time equity, TradingView's margin equity at a typed fill, the hypothetical Flatten's balance for a typed reversal), the freeze, the fill-time exceptions and both lot floors (`tests/test_adapter_sizing_relower.cpp` cases 8, 9, 11: kernel 0.0392 against the percent floor's 0.0391 on a 0.0001 lot), and a staged FX series keeps `Sized` wherever the acceptance coordinate converts at the source's rate (case 12) | F O S | -| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:671, `coof_next_waypoint` pine_adapter.cpp:6808) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5934 and `drain_applied_notifications` (native_execution_consumer.cpp:6595-6608); new behaviour only behind new opt-ins | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints -- on a magnified path at each intrabar's open, twice, and its two extremes, never its close (R5 lane MAG-INTRABAR) (`PineScheduler::recalculate` pine_scheduler_native.cpp:671, `coof_next_waypoint` pine_adapter.cpp:6808) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5948 and `drain_applied_notifications` (native_execution_consumer.cpp:6609-6622); new behaviour only behind new opt-ins | F | | E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:21956-22016) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | | E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:2440) | Kernel recording would duplicate points and shift drawdown | Report policy / ownership (A.1); the adapter declares `KernelRecordedAtHostMarks` (R1): one recorder, at the adapter's own marks | F O | | E7 | **Price grid.** TV does not merely quantize: it tests triggers against a quantized bar while booking the fill on the *unrounded* level, so the adapter shifts the threshold (`source_trigger_threshold` pine_adapter.cpp:343) and books elsewhere | A kernel grid that quantizes both sides is correct and not TV; moving rounding into the kernel would change both native results and the adapter | `NativePriceGrid::None` for the adapter permanently; never "unify". Before L8: a byte-level experiment that the default price spelling on decimal ticks (one-ULP representations) does not move (S) | O S | @@ -1632,7 +1632,7 @@ the rule gives 9.9 + 9.89). | Q1 | Does `engine_script_run_v18` open at L4 and close after L5 ∥ L6, or does L4's sizing hook ship with that batch (§3.4 row 4)? | — (raised by this merge) | | Q2 | Is a hash-domain bump (needed by §2.ii j) acceptable before the R5 final audit (phase table, native-refactor-progress.md:24, *fresh*), or must L12 wait? | F asks; none | | Q3 | Does the fixed-population gate (4190 probes then) compare broker-state hashes across releases, or only trades? If hashes, L4 and L12 need a hash-version plan | O asks; none | -| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:3165); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:3179); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | | Q5 | Liquidation check granularity: `PathAdverseExtreme` default; is a cheaper `CalculationOnly` worth shipping in v1? Pure spec value or injectable policy object (S)? | F: path-accurate default; merged A.3 = spec value + host sizing hook | | Q6 | Does a native series return the current value, a retained series object, or both? It decides C ABI ownership and hash size | S asks; A.5 offers push + current-bar pull | | Q7 | Is a close-reservation key stable across host restarts or only within one `RunIdentity`? | S: identity rules suggest run scope | @@ -1672,11 +1672,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (engine.hpp:1362) + `record_equity_point(script_open_ms)` (engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7984, :8245, :8441, :10214) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` (engine.hpp:1362) + `record_equity_point(script_open_ms)` (engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7998, :8259, :8455, :10232) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). - `KernelRecordedAtHostMarks` (R1, KERNEL-EDGE): the same recording, at the points a C++ host names with `NativeStrategyHost::mark_native_report_point` instead of once per calculation; the consumer never records on its own initiative under it, so the continuation identity stays where `HostRecorded` leaves it. The Pine adapter's existing `mark_script_report_point` route has the same kernel producer. It is what the adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2790), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5222-5264), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5236-5278), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1699,7 +1699,7 @@ struct ScopeFraction { double fraction = 1.0; ScopeClaim claim = ScopeClaim::Gro using ReductionSize = std::variant; // F ``` -- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:709; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5249). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). +- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:709; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5263). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). - `NetOfSiblings`: a fraction-sized reduce claims the bound scope minus the units already claimed by live sibling reduces on it; keys are opaque, never source IDs (S). - Not adopted: S's spec-level `default_sizing` + virtual `resolve_native_sizing` (duplicates the per-request intent, costs an epoch); S's `PercentFreeMargin` (needs L4's free-margin definition) and `EveryCalculation` timing; O's spec-level `size_net_of_fees` (per-request flag, 2 of 3); O's `PercentOfScope` basis (expressed as `ScopeFraction`); S's `ReplaceByKey` / `Fifo` modes (callsite semantics → adapter, R5-4). - TUs: `native_order.{hpp,cpp}`, `native_execution_consumer.cpp`, `native_run_spec.cpp`, the definition hash. @@ -1726,9 +1726,9 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:16163-16184). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:2911), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). +- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:16163-16184). New hashed `RequestDefinition::origin { Host, KernelLiquidation, KernelRisk }`, new `CancelReason::Superseded` on re-price, `MarginCallEvent` joins `CommandEvent` (17 → 18, native_order.hpp:1308). O's alternative, a checkpoint on the excursion walk (`apply_excursion` native_execution_consumer.cpp:2925), is not chosen: a reduction mid-path is the entanglement O itself flags (E2). - FX revaluation (MG9) falls out once `native_fx_curve` drives the mark (`native_fx_curve.cpp`). -- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4975-4988 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4989-5002 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1748,10 +1748,10 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1729-1730) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:7320-7332) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5934). `born_on_remaining_path` (native_execution_consumer.cpp:5930-5934) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1906-1936) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1184-1191, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7177-7184). `on_native_tick` stays the observation hook (S). -4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2855); valid in bar-open, applied and tick callbacks. +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1729-1730) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:7345-7357) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5948). `born_on_remaining_path` (native_execution_consumer.cpp:5944-5948) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1906-1936) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1199-1206, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7191-7198). `on_native_tick` stays the observation hook (S). +4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2869); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:54-106) (O). ### A.5 L6 — native HTF subscriptions (F §2.iv + O G10 + S P7; design in §2.iv) @@ -1817,7 +1817,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:4099-4121, `execute_current` path native_execution_consumer.cpp:4082-4092), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:4099-4121, `execute_current` path native_execution_consumer.cpp:4096-4106), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1837,7 +1837,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. - Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:16998-17090); a toolkit helper is an acceptable stand-in until L9. -- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4975-4988 and the per-point walk), `native_order.hpp`, the state hash. +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4989-5002 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index 4787e1cd9..799a78b03 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -173,7 +173,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Item | What it is | Measured size | Deferred by | Prototype and evidence | Reopens when | | --- | --- | --- | --- | --- | --- | -| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:286`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6307`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | +| D2-B, the quiet-bar protocol and one receipt read per bar | PERF-D2's lane: a per-bar bar-open request, a bulk fold of quiet driver points, the adapter's quiet-open predicate with lazy open bookkeeping, and a gate before the receipt poll. D2-A landed the hook declarations and the O(1) `has_command_after` (`src/native_execution_consumer.hpp:295`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6307`), which still runs about three times a bar | PERF-D2, Linux aarch64 (Cortex-A725), callgrind and GBench: about ×0.88 on the 14 probes (an estimate from a measured −13 to −14 % floor bound for the bar-open request alone); after PB the receipt poll still costs about 250 instructions a bar on the no-order floor. On this tree the quiet-bar probe asks the receipt gate 40,996 times and each once-per-bar-open gate 13,248 times: 3.09 reads a bar, 82 % of them stopped at the gate (macOS arm64, AppleClang 17, Release) | "R5 PERF-D2 DONE 2026-09-24 19:56" (D2-B after V19-D); "R5 SUPERVISOR RULING 2026-09-25 00:15 Taipei": "D2-B (quiet-bar protocol) waits for PERF-D3's verdict"; TRIAGE1 §D | An unpublished prototype branch and its patches. PERF-D3's S1 (next row) prototyped the request and the lazy opening | A post-release performance epoch. Done when the receipt gate is asked as often as the once-per-bar gates | | S1a, kernel quiet-bar driving (PERF-D3) | A bar with nothing live, a flat book and a declined opening is driven in a quiet form: the kernel writes the bookkeeping its four price points would leave (ordinals, the last two driver marks, the floor, the point epochs) without walking them or calling the bar-open callback. Additive host cadence declarations; C hosts declare from their callback table; one C symbol under the PF_API checklist | PERF-D3, Linux aarch64 (Cortex-A725), GBench time and `perf stat` instructions, on the L3 tree `b33ea659`, S1a with S1b: no-order floor 0.764 / 0.660 in time (0.718 / 0.656 in instructions), 14 probes 0.958 / 0.921 (0.892 / 0.856), 100 slots 0.972 / 0.956 per slot. A bare kernel host that opts in: idle 0.545× (35 % fewer instructions a bar), market 0.81×, bracket 0.92×. Byte-identical: corpus 312/312, the 132-configuration battery with its hash columns | "FINDING PERF-D3 (filed for a post-release epoch per the owner's cap)" (ledger, 2026-09-24 16:58 UTC); "OWNER DECISION 2026-09-25 00:20 Taipei"; TRIAGE1 §D: "PERF-D3 S1/S2 (post-release epoch)" | An unpublished prototype branch (five commits on L3's tree) and its patches | The post-release epoch, with the owner calls PERF-D3 lists (the API under the existing ruling, the restated gate count) | | S1b, the Pine lazy opening | The Pine host records what its skipped opening and its uncalled input callback would have written, at the bar's first host callback. Member functions only, no layout change | Measured with S1a. A quiet bar then asks 15 gates, not 30, so the quiet-bar cost check `CHECK(asked >= 20 * o.boundary.size())` (`tests/test_adapter_quiet_bar.cpp:1054`) must be restated | As S1a | As S1a. The in-script hash witnesses caught the prototype's one miss (`awaiting_legacy_script_open_ms_`) | As S1a | | S1c, the held-position wake band | S1 for bars that hold a position with nothing live, 41 % of the median public slot's bars. The host declares a price band, and the kernel calls the opening only when the bar's range leaves it. The adapter derives the band from its margin model and declines when it cannot bound it | PERF-D3, a measurement-only upper bound without the band, which is not sound in general: 100 slots 0.776 / 0.716 per slot, 14 probes 0.830 / 0.761, cumulative against L3 with S1, S2 and D2-A | As S1a; the owner call "Fund the price band" | An unpublished measurement-only script | As S1a. Size L, parity risk high: TradingView's margin money rules | @@ -184,7 +184,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4791`) and again in `apply_execution` (`src/native_order.cpp:4805`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:546`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | | A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17130`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22011`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22248`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:33`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:17114`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, Linux aarch64, callgrind on the floor). PERF-K1: `ScopedTimezone` copies the zone name, which allocates for names of 16 or more characters under libstdc++ | TRIAGE1 §D: "D2-D finding 4 (the timezone `localtime_r` memo)", under the owner's cap. D2-D's report: an exact zoned memo "would need the zone's transition table, which belongs in `timezone.cpp`" | None; the UTC memo's witness is `tests/test_chart_day_memo.cpp` | A lane that owns `src/timezone.cpp` and builds an exact zone-transition memo (PERF-K1's Sitka case shows why a shortcut is not exact), byte-identical, with the zoned keys of `test_chart_day_memo` still equal to the computed ones | -| Thread-local reads that remain per call | D2-C moved the three scopes' writes onto the pump's block, `pump_ambient` (`src/native_execution_consumer.hpp:461`). A reader still reaches the block through `tl_runtime_ambient` (`src/ta_extremes_volume.cpp:29`): `bar_context` (`src/ta_extremes_volume.cpp:82`), read by every `ExtremeRing::update` (`src/ta_extremes_volume.cpp:117`), which backs `ta.highest`, `ta.lowest`, `ta.highestbars` and `ta.lowestbars` and, through an embedded `Highest` and `Lowest`, `ta.stoch`, `ta.wpr` and `ta.range`; and `matching_day_partition` (`src/timeframe.cpp:628`), read by the default-anchored `ta.vwap` through `session_day_index`, by `crosses_boundary` on a DAY period (`timeframe.change("D")`) and by the session-period helpers. `decompose_ms_local` (`src/session_time.cpp:250`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:605`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (Linux aarch64, GCC 13, callgrind). INT23: 2.00 a bar on the zoned probe 064, 0.08 on probe 008 | None for the TA and day-partition readers: "RULING: INT23 ACCEPTED" (ledger, 2026-09-24 23:30 UTC) reads "dlopen TLS 13->0/bar", and INT23's report names only the timezone and order-core residuals. This row is their record | Unpublished (AUDIT4's scratch) | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | +| Thread-local reads that remain per call | D2-C moved the three scopes' writes onto the pump's block, `pump_ambient` (`src/native_execution_consumer.hpp:470`). A reader still reaches the block through `tl_runtime_ambient` (`src/ta_extremes_volume.cpp:29`): `bar_context` (`src/ta_extremes_volume.cpp:82`), read by every `ExtremeRing::update` (`src/ta_extremes_volume.cpp:117`), which backs `ta.highest`, `ta.lowest`, `ta.highestbars` and `ta.lowestbars` and, through an embedded `Highest` and `Lowest`, `ta.stoch`, `ta.wpr` and `ta.range`; and `matching_day_partition` (`src/timeframe.cpp:628`), read by the default-anchored `ta.vwap` through `session_day_index`, by `crosses_boundary` on a DAY period (`timeframe.change("D")`) and by the session-period helpers. `decompose_ms_local` (`src/session_time.cpp:250`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:605`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (Linux aarch64, GCC 13, callgrind). INT23: 2.00 a bar on the zoned probe 064, 0.08 on probe 008 | None for the TA and day-partition readers: "RULING: INT23 ACCEPTED" (ledger, 2026-09-24 23:30 UTC) reads "dlopen TLS 13->0/bar", and INT23's report names only the timezone and order-core residuals. This row is their record | Unpublished (AUDIT4's scratch) | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | | "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | ### Recorded witness drift @@ -194,9 +194,9 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. K4, the batch log pre-size. Since V19-B's step 6 (`08e71bae`) a run under the default `NativeEventRetention::Window` keeps no driver point and retires its journal at every script-bar boundary, so K4 has nothing to size. -`reserve_driver_log` (`src/native_execution_consumer.cpp:2870-2875`) returns +`reserve_driver_log` (`src/native_execution_consumer.cpp:2884-2889`) returns unless the retention is Full, and `presize_logs` -(`src/native_execution_consumer.cpp:9655-9666`) sizes no journal under Window. +(`src/native_execution_consumer.cpp:9669-9680`) sizes no journal under Window. `NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp:714`) defaults to Window, and the Pine adapter declares `NativeEventRetention::Window` (`src/source/pine_adapter.cpp:2931`). K4 still diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index e085fe0c8..e180ced9a 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -470,7 +470,7 @@ 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:9761-9803`), which admits the begin, checks the +(`native_execution_consumer.cpp:9810-9852`), 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 @@ -498,7 +498,7 @@ 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:2723-2811`), under the same legality rule; see *Driving the +(`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` @@ -638,13 +638,13 @@ 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:5930-5934`). Requests accepted before the +`native_execution_consumer.cpp:5944-5948`). 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:7420-7422`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7434-7436`). **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:7390`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:7404`), 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 @@ -2112,7 +2112,7 @@ default, set while no run is active — because each row is a full 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:8302`), so +(`record_script_report_point`, `native_execution_consumer.cpp:8316`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -3086,7 +3086,7 @@ 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:8384`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:8398`). 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 @@ -3175,7 +3175,7 @@ These are existing refusals, not implied future features: 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:2723`) obeys the one legality +`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 @@ -4000,6 +4000,27 @@ 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 it refused without beginning (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, and leaves the lifecycle, the run +identity and the retained rows as it found them. 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. + **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 diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index b3baba9fb..b3a07cbb0 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -115,7 +115,7 @@ the rest from the chart. A native host declares *everything* in one `NativeRunSpec` (native_run_spec.hpp:622) applied by `configure_native` (native_host.hpp:1196); a C host sends `pf_native_run_spec_v1` (pineforge.h:510) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2310) -through `strategy_configure_native_ext_v1` (native_c_api.h:3156). Setup is +through `strategy_configure_native_ext_v1` (native_c_api.h:3175). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. @@ -126,9 +126,9 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:639 | `currency` pineforge.h:499 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:636, `tickerid` native_run_spec.hpp:637, `type` native_run_spec.hpp:638, `basecurrency` native_run_spec.hpp:640, `description` native_run_spec.hpp:641 and `volumetype` native_run_spec.hpp:642 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:502 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:658 | `fee_value` pineforge.h:502 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:4117-4118. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7986-7987, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:664 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:3263). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 13, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:4131-4132. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:8000-8001, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:664 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:3277). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 13, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2147 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2151 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | | `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2321 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:695. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | @@ -280,23 +280,23 @@ Everything here is one call: `submit` native_host.hpp:1213, or `submit_market` native_host.hpp:1227 for the market-only shorthand, which *refuses* non-market extras rather than dropping them. `submit` returns a `RequestHandle` native_order_identity.hpp:135; keep it, it is the order id. In C the same call -is `strategy_native_submit_v1` native_c_api.h:2723 over a +is `strategy_native_submit_v1` native_c_api.h:2742 over a `pf_native_request_v1` native_c_api.h:2138. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2723 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | -| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2723 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | -| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2723 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2742 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | +| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2742 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | +| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2742 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | | `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:354 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:459 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | | `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:337 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2810 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2789 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2829 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2808 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | | `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:478 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:479 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:472 | `hello_kernel.cpp` | The default: no group, no sibling effect. | | `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:3252. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:3266. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:502 | `native_sized_report_strategy.cpp` | `fee_value` is percent ÷ 100 of the absolute account notional. | | `strategy.commission.cash_per_contract` | `NativeFeeKind::CashPerUnit` native_run_spec.hpp:21 | `fee_kind` pineforge.h:502 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -322,7 +322,7 @@ when a rejection's `RequestRejectReason` native_order.hpp:821 matters. | `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:392 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | | `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2131-2138 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | | `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2137-2141 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | -| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2161 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:3035). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | +| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2161 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:3054). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | `limit=` and `stop=` are listed once here rather than once per command, because a native trigger is a property of the `Request` native_order.hpp:483 @@ -377,7 +377,7 @@ of a predicate, and it reports the kernel verbatim too: `ReplaceResult` and `SubmitResult` of every command that actually reached the kernel, and `cancel_outcome` says whether a cancel was commanded at all. Pine's `strategy.cancel` tells a script neither. In C -each leg is its own `strategy_native_submit_v1` native_c_api.h:2723 with +each leg is its own `strategy_native_submit_v1` native_c_api.h:2742 with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:458, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` native_c_api.h:2221-2222, so a C bracket child arms after its owner's print or over @@ -405,7 +405,7 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:739 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:665 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:3252. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:665 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:3266. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:663 | `max_abs_units` pineforge.h:504 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | | `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2357 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | | `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2360 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -462,8 +462,8 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2535 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1791 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2506 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | -| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:3020 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5536. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:3039 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5550. | | raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2504 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2545 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1485 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | @@ -534,14 +534,14 @@ range-end reporting is off. | --- | --- | --- | --- | --- | | `strategy.account_currency` | `currency` native_run_spec.hpp:639 | `currency` pineforge.h:499 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:650 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | | `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:648 | `initial_capital` pineforge.h:501 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `native_marked_equity(mark)` native_host.hpp:1301; on the Pine host `current_equity() + open_profit(mark)` pine_strategy_host.hpp:664 | `strategy_native_marked_equity_v1` native_c_api.h:3060 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341, which is Pine's equity: TradingView charges the open lots' paid entry fees. | +| `strategy.equity` | `native_marked_equity(mark)` native_host.hpp:1301; on the Pine host `current_equity() + open_profit(mark)` pine_strategy_host.hpp:664 | `strategy_native_marked_equity_v1` native_c_api.h:3079 | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341, which is Pine's equity: TradingView charges the open lots' paid entry fees. | | `strategy.netprofit` | on the Pine host `net_profit` pine_strategy_host.hpp:663; `net_profit` engine.hpp:967 is the closed trades' figure | `net_profit` pineforge.h:374, less open lots' `entry_commission` | `native_open_lots_strategy.cpp` | The closed trades' net profit less the entry fees the open lots have paid. The report's figure is the closed trades'. | | `strategy.netprofit_percent` | derive: `net_profit` pine_strategy_host.hpp:663 over `initial_capital` native_run_spec.hpp:648 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | | `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | | `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | Of initial capital. | | `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:224 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | | `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3060 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | +| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3079 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | | `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1025 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the initial capital plus `strategy.netprofit`, the open lots' paid entry fees charged. | | `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:274 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | | `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:274 | `native_sized_report_strategy.cpp` | Same rule. | @@ -559,10 +559,10 @@ range-end reporting is off. | `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1852 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | | `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1853 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | | `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1854 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | -| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2836 | `hello_kernel.cpp` | Signed: no separate direction field. | -| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2836 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2877 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | -| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3068 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | +| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2855 | `hello_kernel.cpp` | Signed: no separate direction field. | +| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2855 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2896 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | +| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3087 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | | `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:546 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:650 is one scalar and `configure_native_fx_curve` native_host.hpp:1202 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | | `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:546 | `tests/test_native_fx_curve.cpp` | Same ruling. | @@ -572,8 +572,8 @@ range-end reporting is off. `native_open_lots(mark)` native_host.hpp:1297 answers one `NativeOpenLot` native_host.hpp:328 per open physical lot, in book order (oldest first, `NativePhysicalPosition::lot_count` native_host.hpp:299 rows). The C twin is -`strategy_native_open_lot_count_v1` native_c_api.h:2869 then -`strategy_native_open_lot_get_v1` native_c_api.h:2877 into a +`strategy_native_open_lot_count_v1` native_c_api.h:2888 then +`strategy_native_open_lot_get_v1` native_c_api.h:2896 into a `pf_native_open_lot_v1` native_c_api.h:1767, whose two strings borrow the snapshot until the next count call. `mark` is the price the three marked fields are computed at; Pine's builtins mark at the current `close`. Reading it moves @@ -587,7 +587,7 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2869 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2888 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | | `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1767 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | | `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1767 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | | `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1767 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | @@ -662,8 +662,8 @@ classifies its own rows by order family instead. Which leg closed a row is its | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:491 in `subscriptions` native_run_spec.hpp:701, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2941 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:493 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:799 and `pf_native_gaps_e` native_c_api.h:810 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. Another symbol's bars are an instrument feed: `NativeInstrumentFeed` native_run_spec.hpp:580 in `instrument_feeds` native_run_spec.hpp:711, read by a series whose source is `InstrumentFeed` native_run_spec.hpp:488 (C++ only in 1.0: @ref native_engine, "Instrument feeds: another symbol's bars"). | -| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:524 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:487 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3218 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:491 in `subscriptions` native_run_spec.hpp:701, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2960 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:493 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:799 and `pf_native_gaps_e` native_c_api.h:810 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. Another symbol's bars are an instrument feed: `NativeInstrumentFeed` native_run_spec.hpp:580 in `instrument_feeds` native_run_spec.hpp:711, read by a series whose source is `InstrumentFeed` native_run_spec.hpp:488 (C++ only in 1.0: @ref native_engine, "Instrument feeds: another symbol's bars"). | +| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:524 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:487 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3237 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | | `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:495 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:811 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | | `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:495 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:813 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | | `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:494 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:800 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | @@ -752,7 +752,7 @@ The host is a `pf_native_callbacks_v1` native_c_api.h:2499 table handed to `strategy_native_host_create_v1` native_c_api.h:2682; the run is `pf_native_run_spec_v1` pineforge.h:510 plus `pf_native_run_spec_ext_v1` native_c_api.h:2310 through `strategy_configure_native_ext_v1` -native_c_api.h:3156, and `strategy_native_run_v1` native_c_api.h:2695 drives +native_c_api.h:3175, and `strategy_native_run_v1` native_c_api.h:2714 drives the bars into a `pf_report_t` pineforge.h:431. Three rules govern the C door: @@ -762,7 +762,7 @@ Three rules govern the C door: with a documented negative status and mutates nothing. 2. **A callback must not unwind, and must return 0.** A non-zero return latches `NativeFailureCode::CallbackException` native_host.hpp:69 and ends the run - `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2922. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2941. 3. **Installing `on_recalculate` replaces `on_bar`.** A C host that installs only the recalculate hook and still expects the bar hook silently trades nothing. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 3aca9e364..40c7e864a 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -2690,8 +2690,27 @@ PF_API void strategy_native_host_free(pf_strategy_t s); * The specification must already be Ready (#strategy_configure_native_v1). * @p out may be NULL to skip reporting; otherwise its arrays are * heap-allocated and released by #strategy_native_report_free_v1. - * @return PF_NATIVE_OK when the run reached Completed, PF_NATIVE_E_RUN_FAILED - * when it did not (read #strategy_native_state_v1 for the code). */ + * + * A report belongs to THIS attempt or is empty, and a refused attempt never + * answers PF_NATIVE_OK. A call the host refuses without beginning (a handle + * that is not Ready: Unconfigured, already Completed or Failed; a refused bar + * array or run option) leaves the lifecycle, the run identity and the rows + * the handle retains exactly as they were, answers PF_NATIVE_E_RUN_FAILED and + * leaves @p out as the empty report (every array NULL, every count 0), + * overwritten and never freed, so it is safe whether or not the caller + * initialised it and #strategy_native_report_free_v1 on it is a no-op; the + * reason is in #strategy_get_last_error, #strategy_get_last_error_code and + * #strategy_last_run_status (1). A run that began and then failed answers + * PF_NATIVE_E_RUN_FAILED too, leaves the handle Failed and keeps its partial + * rows in @p out, as before. A Completed handle runs again after a successful + * #strategy_configure_native_v1 (or #strategy_configure_native_ext_result_v1). + * The refusals made before the engine sees the call, PF_NATIVE_E_HANDLE and + * PF_NATIVE_E_ARGUMENT, leave @p out untouched. The gate is the engine's own + * report reader, so every other run entrypoint obeys it; a call refused over + * a live stream reads empty, and #strategy_stream_fill_report keeps its rows. + * @return PF_NATIVE_OK when this call's run reached Completed, + * PF_NATIVE_E_RUN_FAILED when it did not (read #strategy_native_state_v1 for a + * Failed code, #strategy_get_last_error for a refusal). */ PF_API int strategy_native_run_v1(pf_strategy_t s, const pf_bar_t* bars, int n, pf_report_t* out); diff --git a/src/c_abi.cpp b/src/c_abi.cpp index bdb3dc18f..6d2234981 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -54,10 +54,10 @@ #include #include #include - namespace pineforge { void clear_native_c_host_refusal_error(BacktestEngine* engine) noexcept; bool native_c_host_in_postrun_hook_frame(BacktestEngine* engine) noexcept; +void native_stream_snapshot_report(BacktestEngine* engine, ReportC* out); } namespace { @@ -767,8 +767,8 @@ PF_API int strategy_stream_end(pf_strategy_t s, int finalize_partial_input_bar) PF_API int strategy_stream_fill_report(pf_strategy_t s, pf_report_t* out) { return pf_cabi_int(s, [&] { if (!s || !out) return -1; - static_cast(s)->fill_report( - reinterpret_cast(out)); + pineforge::native_stream_snapshot_report( + static_cast(s), reinterpret_cast(out)); return 0; }); } diff --git a/src/engine_report.cpp b/src/engine_report.cpp index 1ac012e16..a88a6c00c 100644 --- a/src/engine_report.cpp +++ b/src/engine_report.cpp @@ -3,6 +3,7 @@ */ #include "engine_internal.hpp" +#include "native_execution_consumer.hpp" #include @@ -37,7 +38,26 @@ void BacktestEngine::trace(const std::string& name, double value) { } +// The report of the latest run or stream_begin call this engine was given, or +// the empty report. The engine keeps the rows of the last run it BEGAN, and a +// call that never began (a begin outside Ready, a refused bar array or run +// option, a calendar or timezone refusal) leaves them in place on purpose, so +// the rows alone cannot say which call they answer. Every batch reader shares +// this one gate: strategy_native_run_v1, the native module wrappers and the +// generated run_backtest / run_backtest_full, which call run() and then this. +// A call that began owns its rows whatever became of it (a Completed report and +// a started-then-failed run's partial rows publish unchanged); a call that did +// not, or one refused over a live attempt (a batch call while a stream is live), +// owns none: `*out` is overwritten with the empty report (every array NULL, +// every count zero) and never freed, since what it held is not this call's to +// release. strategy_stream_fill_report, the explicit snapshot, runs this same +// function asking for the rows' own owner (NativeExecutionConsumer:: +// fill_snapshot_report), so a live stream's rows survive such a refusal. void BacktestEngine::fill_report(ReportC* out) const { + if (!NativeExecutionConsumer::rows_are_current(*this)) { + *out = ReportC{}; + return; + } fill_trades_section(out); out->input_bars_processed = diag_input_bars_processed_; diff --git a/src/native_c_host.cpp b/src/native_c_host.cpp index 443a2f91d..e59cc1c14 100644 --- a/src/native_c_host.cpp +++ b/src/native_c_host.cpp @@ -3460,9 +3460,22 @@ PF_API int strategy_native_run_v1(pf_strategy_t s, const pf_bar_t* bars, int n, auto* host = host_of(s); if (!host) return PF_NATIVE_E_HANDLE; if (n < 0 || (n > 0 && !bars)) return c_refuse(host, PF_NATIVE_E_ARGUMENT); + /* OK answers a run THIS call carried to Completed. A call the engine + * refuses without beginning (a Completed handle nothing re-armed, a + * Failed or not-yet-Ready handle, a refused bar array or run option) + * leaves the lifecycle, the run identity and the previous rows as they + * were, so the lifecycle alone cannot tell it from a finished run: the + * consumed run-number high-water can, because begin_ready moves it, + * strictly upward, for every attempt that gets past admission. The + * report is not decided here: BacktestEngine::fill_report publishes + * the rows of the latest attempt (a started-then-failed run's partial + * rows included) and the empty report for an attempt that did not + * begin. */ + const std::uint64_t high_water = host->native_consumed_high_water(); host->run(reinterpret_cast(bars), n); if (out) host->fill_report(reinterpret_cast(out)); return host->native_state().kind == pineforge::NativeLifecycleKind::Completed + && host->native_consumed_high_water() > high_water ? PF_NATIVE_OK : PF_NATIVE_E_RUN_FAILED; }); diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 0c1d95a6e..b1f0411cd 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1339,6 +1339,18 @@ RunFailureValue NativeExecutionConsumer::failure_value_of(const BacktestEngine& return RunFailureValue{RunFailureCode::engine_unclassified_error, nullptr}; } +NativeExecutionConsumer::AttemptScope::AttemptScope(NativeExecutionConsumer& c) noexcept + : c_(c), nested_(c.in_attempt_call_), over_live_(!nested_ && (c.running_spec_ + || c.in_callback_ || c.processing_input_ || c.preparing_begin_)) { + if (!nested_) c_.in_attempt_call_ = true; + if (!nested_ && !over_live_) c_.rows_current_ = c_.batch_current_ = false; +} + +NativeExecutionConsumer::AttemptScope::~AttemptScope() { + if (!nested_) c_.in_attempt_call_ = false; + c_.batch_current_ = !nested_ && !over_live_ && c_.rows_current_; +} + void NativeExecutionConsumer::present_refusal(BacktestEngine& engine, const char* text, RunFailureCode code, const RunFailureArgs& args) { @@ -2481,6 +2493,8 @@ bool NativeExecutionConsumer::begin_ready(BacktestEngine& engine, NativeRunPhase return false; } engine.reset_run_state(); + // The rows are this attempt's from here on, whether it completes or fails. + rows_current_ = batch_current_ = true; // RULING A48: one generic capability, wired once per run. A host that // declares ownership supplies the closing-row magnitudes; the kernel then // keeps no excursion model of its own for this run. @@ -9716,6 +9730,7 @@ void NativeExecutionConsumer::pump_batch(BacktestEngine& engine, const Bar* bars } void NativeExecutionConsumer::run_simple(BacktestEngine& engine, const Bar* bars, int n) { + const AttemptScope attempt(*this); NativeBeginArgs args{bars, n, {}, {}, false, 4, MagnifierDistribution::ENDPOINTS, engine.magnifier_volume_weighted_, 2}; args.simple_run = true; @@ -9750,6 +9765,7 @@ void NativeExecutionConsumer::run_tf(BacktestEngine& engine, const std::string& script_tf, bool bar_magnifier, int magnifier_samples, MagnifierDistribution magnifier_dist) { + const AttemptScope attempt(*this); const NativeBeginArgs args{input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, engine.magnifier_volume_weighted_, 2}; if (!prepare_public_begin(engine, args)) return; @@ -9799,6 +9815,7 @@ void NativeExecutionConsumer::run_rich(BacktestEngine& engine, const void* overrides, bool bar_magnifier, int magnifier_samples, MagnifierDistribution magnifier_dist) { + const AttemptScope attempt(*this); NativeBeginArgs args{input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, engine.magnifier_volume_weighted_, 2}; args.inputs = &inputs; @@ -9839,6 +9856,7 @@ bool NativeExecutionConsumer::stream_begin(BacktestEngine& engine, const Bar* warmup_bars, int n_warmup, const std::string& input_tf, const std::string& script_tf) { + const AttemptScope attempt(*this); // Preserve the live stream before asking the provider to stage/configure a // new run. The legacy route diagnoses this state first; in particular, // no warmup copy, adapter reset, or broker/spec mutation may occur. @@ -11472,8 +11490,54 @@ std::uint64_t NativeStrategyHost::broker_state_hash_projection() const { return broker_state_hash_from_execution_hash(execution); } +// The report gate (BacktestEngine::fill_report), answering for the latest +// public run / stream_begin CALL. An AttemptScope is opened first by every such +// call. A call entered while nothing is live is an attempt: it disowns the +// retained rows until begin_ready's reset hands them back, so a refusal before +// the reset (not Ready, bad bars or option, calendar refusal) reports empty and +// a started-then-failed run keeps its partial rows. A call entered over a live +// attempt (a stream between its pushes, a callback or an input in flight) is +// refused by admission: it owns no rows and leaves rows_current_ alone, but its +// exit clears batch_current_, so the report that call's wrapper reads right +// after it is empty; a call nested inside another attempt call closes before the +// outer one, whose exit sets batch_current_ from its own outcome. Bools only: +// nothing counts, so nothing wraps; nothing here is hashed or read by a run. +bool NativeExecutionConsumer::rows_are_current(const BacktestEngine& engine) noexcept { + const auto* consumer = static_cast( + engine.execution_consumer_slot_.ptr.get()); + return consumer == nullptr || consumer->batch_current_; +} + +// The explicit snapshot reader: the same fill_report, the same gate, asked for +// the rows' own owner (rows_current_) instead of the latest call's outcome, so +// a live stream's snapshot survives a batch call refused over it. +void NativeExecutionConsumer::fill_snapshot_report(const BacktestEngine& engine, + ReportC* out) { + auto* consumer = static_cast( + engine.execution_consumer_slot_.ptr.get()); + if (consumer == nullptr) { + engine.fill_report(out); + return; + } + const bool outcome = consumer->batch_current_; + consumer->batch_current_ = consumer->rows_current_; + try { + engine.fill_report(out); + } catch (...) { + consumer->batch_current_ = outcome; + throw; + } + consumer->batch_current_ = outcome; +} + } // inline namespace engine_script_run_v19 +// The C stream seam's snapshot reader, declared in c_abi.cpp beside the native +// C host's hooks: strategy_stream_fill_report reads through it. +void native_stream_snapshot_report(BacktestEngine* engine, ReportC* out) { + NativeExecutionConsumer::fill_snapshot_report(*engine, out); +} + // The portable run-spec digest lives here, in the one translation unit that // owns `hash_spec` (see its definition above, beside the continuation fold), // so the digest cannot drift from the fields the continuation identity folds diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index e9a445836..7ec496cd5 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -266,6 +266,15 @@ class NativeExecutionConsumer final : public IExecutionConsumer { static const char* failure_code_of(const BacktestEngine& engine) noexcept; static const char* failure_args_of(const BacktestEngine& engine) noexcept; static RunFailureValue failure_value_of(const BacktestEngine& engine) noexcept; + // Whether the rows `engine` retains answer the latest run or stream_begin + // call it was given (the ledger below): false after a call that never + // reached begin_ready's reset_run_state() or was refused over a live + // attempt; true after one that began (a started-then-failed run owns its + // partial rows) and with no consumer bound. BacktestEngine::fill_report + // gates on it; fill_snapshot_report (the C stream seam) reads the rows of + // the latest attempt that reset them, through the same fill_report. + static bool rows_are_current(const BacktestEngine& engine) noexcept; + static void fill_snapshot_report(const BacktestEngine& engine, ReportC* out); std::vector events_after(uint64_t after_ordinal) const; // The command rows of events_after(after_ordinal), read in place (R5 lane // PERF-P4). The history is in ordinal order, so those rows are exactly @@ -1001,6 +1010,12 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // std::string's own. static bool same_bytes(const std::string& a, const std::string& b) noexcept; bool begin_ready(BacktestEngine& engine, NativeRunPhase phase, int64_t initial_floor_ms); + class AttemptScope { // opened first by every public run / stream_begin call + NativeExecutionConsumer& c_; const bool nested_; const bool over_live_; + public: + explicit AttemptScope(NativeExecutionConsumer& c) noexcept; + ~AttemptScope(); + }; bool prepare_public_begin(BacktestEngine& engine, const NativeBeginArgs& args); bool apply_staged_ingress(BacktestEngine& engine); bool refuse_mixed_input_mode(BacktestEngine& engine, InputMode requested); @@ -1561,6 +1576,15 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // tests/test_native_callback_caches.cpp. friend struct NativeExecutionConsumerProbe; uint64_t consumed_high_water_ = 0; + // The attempt ledger, report presentation only, never a run input, hashed + // or reset by a run. rows_current_: the retained rows belong to the latest + // attempt that could own them (set by begin_ready after the reset, cleared + // by a call entered while nothing is live). batch_current_: the latest + // call began (what fill_report gates on; cleared by a call over a live + // attempt, restored by the outermost call's exit). in_attempt_call_: open. + bool rows_current_ = true; + bool batch_current_ = true; + bool in_attempt_call_ = false; std::string bound_session_key_; native_order::WorkingRequestCore requests_{{"unbound", 1}}; uint64_t next_timeline_ordinal_ = 1; diff --git a/tests/test_checked_settings.cpp b/tests/test_checked_settings.cpp index cd1f0d04c..daad09c45 100644 --- a/tests/test_checked_settings.cpp +++ b/tests/test_checked_settings.cpp @@ -78,6 +78,142 @@ void test_refused_report_allocations() { std::cout << "latched report refusal: legacy, full and checked allocation regression PASS\n"; } +// The generated run_backtest / run_backtest_full (tests/fixtures/checked_settings/ +// generated.cpp, emitted by the transpiler) call run() and then fill_report(), +// unchanged. A run the engine refuses without beginning -- a bar array that +// fails preflight -- must not hand out the rows of the run before it: a report +// belongs to its own attempt or is empty, and the next valid run on the same +// handle is current again. +void test_refused_rerun_report_is_empty() { + std::vector bars; + for (int index = 0; index < 60; ++index) { + const double price = 100 + index % 7; + bars.push_back({price, price + 2, price - 1, price + 1, 5, + 1577836800000LL + static_cast(index) * 60000}); + } + auto invalid = bars; + invalid[0].open = std::numeric_limits::quiet_NaN(); + for (int mode = 0; mode < 2; ++mode) { + pf_strategy_t strategy = strategy_create(nullptr); + assert(strategy); + pf_report_t first{}; + run_backtest_full(strategy, bars.data(), 60, "1", "1", 0, 4, + PF_MAGNIFIER_ENDPOINTS, &first); + assert(first.total_trades > 0 && first.trades && first.trades_len == first.total_trades); + pf_report_t refused{}; + if (mode == 0) { + run_backtest(strategy, invalid.data(), 60, &refused); + } else { + run_backtest_full(strategy, invalid.data(), 60, "1", "1", 0, 4, + PF_MAGNIFIER_ENDPOINTS, &refused); + } + assert(!std::string(strategy_get_last_error(strategy)).empty()); + assert(refused.total_trades == 0 && refused.trades_len == 0 && !refused.trades); + assert(refused.equity_curve_len == 0 && !refused.equity_curve); + report_free(&refused); + pf_report_t again{}; + run_backtest_full(strategy, bars.data(), 60, "1", "1", 0, 4, + PF_MAGNIFIER_ENDPOINTS, &again); + assert(again.total_trades == first.total_trades && again.trades_len == first.trades_len); + report_free(&again); + report_free(&first); + strategy_free(strategy); + } + std::cout << "refused rerun on a generated handle: empty report, next run current PASS\n"; +} + +// A live stream's snapshot is current. stream_begin makes the stream the +// handle's latest attempt, so strategy_stream_fill_report holds its rows while +// it runs and after it ends; an attempt the engine then refuses without +// beginning (a bar array that fails preflight) leaves nothing to hand out, and +// the snapshot is the empty report, never the stream's rows under a refused +// call. +void test_stream_snapshot_is_current() { + std::vector bars; + for (int index = 0; index < 60; ++index) { + const double price = 100 + index % 7; + bars.push_back({price, price + 2, price - 1, price + 1, 5, + 1577836800000LL + static_cast(index) * 60000}); + } + pf_strategy_t strategy = strategy_create(nullptr); + assert(strategy); + assert(strategy_stream_begin(strategy, bars.data(), 40, "1", "1") == 0); + pf_report_t live{}; + assert(strategy_stream_fill_report(strategy, &live) == 0); + assert(live.script_bars_processed > 0); + report_free(&live); + for (int index = 40; index < 60; ++index) + assert(strategy_stream_push_bar(strategy, &bars[index]) == 0); + assert(strategy_stream_end(strategy, 0) == 0); + pf_report_t ended{}; + assert(strategy_stream_fill_report(strategy, &ended) == 0); + assert(ended.script_bars_processed > 0); + report_free(&ended); + auto invalid = bars; + invalid[0].open = std::numeric_limits::quiet_NaN(); + pf_report_t refused{}; + run_backtest(strategy, invalid.data(), 60, &refused); + assert(!std::string(strategy_get_last_error(strategy)).empty()); + report_free(&refused); + pf_report_t after{}; + assert(strategy_stream_fill_report(strategy, &after) == 0); + assert(after.total_trades == 0 && after.trades_len == 0 && !after.trades); + assert(after.input_bars_processed == 0 && after.script_bars_processed == 0); + report_free(&after); + strategy_free(strategy); + std::cout << "stream snapshot: live and ended rows current, refused attempt leaves the empty report PASS\n"; +} + +// A batch call refused over a live stream hands out no stream payload as ITS +// report, and the stream's own snapshot still reads the stream's rows. Both +// sides run through the unchanged generated wrappers (run_backtest, run_backtest_full) +// and the C stream seam: admission refuses a begin while the stream runs, the +// wrapper's fill_report then answers the refused call (empty), while +// strategy_stream_fill_report asks for the rows' own owner and gets the stream's. +// Defined fields only: the bar counters and the trade count. +void test_refused_batch_over_live_stream() { + std::vector bars; + for (int index = 0; index < 60; ++index) { + const double price = 100 + index % 7; + bars.push_back({price, price + 2, price - 1, price + 1, 5, + 1577836800000LL + static_cast(index) * 60000}); + } + for (int mode = 0; mode < 2; ++mode) { + pf_strategy_t strategy = strategy_create(nullptr); + assert(strategy); + assert(strategy_stream_begin(strategy, bars.data(), 40, "1", "1") == 0); + for (int index = 40; index < 50; ++index) + assert(strategy_stream_push_bar(strategy, &bars[index]) == 0); + pf_report_t before{}; + assert(strategy_stream_fill_report(strategy, &before) == 0); + assert(before.script_bars_processed > 0); + // The batch side: a valid array, refused only because the stream is live. + pf_report_t batch{}; + batch.total_trades = 99; + if (mode == 0) { + run_backtest(strategy, bars.data(), 60, &batch); + } else { + run_backtest_full(strategy, bars.data(), 60, "1", "1", 0, 4, + PF_MAGNIFIER_ENDPOINTS, &batch); + } + assert(!std::string(strategy_get_last_error(strategy)).empty()); + assert(batch.total_trades == 0 && batch.trades_len == 0 && !batch.trades); + assert(batch.input_bars_processed == 0 && batch.script_bars_processed == 0); + assert(batch.equity_curve_len == 0 && !batch.equity_curve); + report_free(&batch); + // The snapshot side: the stream's own rows, unchanged by the refusal. + pf_report_t after{}; + assert(strategy_stream_fill_report(strategy, &after) == 0); + assert(after.script_bars_processed == before.script_bars_processed); + assert(after.input_bars_processed == before.input_bars_processed); + assert(after.total_trades == before.total_trades); + report_free(&after); + report_free(&before); + strategy_free(strategy); + } + std::cout << "batch refused over a live stream: empty batch report, stream snapshot keeps its rows PASS\n"; +} + std::string receipt_field(const std::string& document, const std::string& name, const std::string& field) { const auto row = document.find("{\"name\":\"" + name + "\""); @@ -122,6 +258,7 @@ void operator delete[](void* allocation, const std::nothrow_t&) noexcept { ::ope int main(int argc, char** argv) { test_refused_report_allocations(); + test_refused_rerun_report_is_empty(); if (argc == 2 && std::string(argv[1]) == "--report-leak-only") return 0; using namespace pineforge::checked_settings; assert(number(0xffff0000LL) == "4294901760"); @@ -368,5 +505,7 @@ int main(int argc, char** argv) { report_free(&report); strategy_free(configured); strategy_free(strategy); + test_stream_snapshot_is_current(); + test_refused_batch_over_live_stream(); std::cout << "checked settings: validation, receipt and exception containment PASS\n"; } diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index cefd9fdbd..39d4825f0 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -9340,6 +9340,890 @@ static void check_csurface_create_and_configure_refusals(void) { } } +/* ── PX native-attempt characterization (lane px-native-attempt-repro) ───── + * + * strategy_native_run_v1 on a handle whose FIRST run completed, followed by a + * SECOND call the kernel refuses. The header documents PF_NATIVE_OK as "the + * run reached Completed" and the filled report as the run's own. These rows + * pin what a refused second attempt must answer, one group per refusal cause: + * + * lifecycle no re-arm between the two calls + * bad settings a refused re-arm: a pre-kernel typed refusal (handle stays + * Completed), a legacy invalid spec (latches Failed) and a + * reused session key (latches Failed) + * bad feed a successfully RE-ARMED (Ready) handle, then a bar array the + * preflight refuses. A feed refused only because the handle is + * not Ready is lifecycle coverage, never feed coverage. + * capability a successfully re-armed handle whose retained intrabar path + * fails its run-time preflight + * + * Every attempt records the actual return code, last_run_status, last error + * text and code, native state and a DEEP COPY of the report's defined fields + * (never struct bytes, pointers or padding). A scenario whose refusal is not + * the cause it names prints "PXCAUSE ... NOT-REACHED" and asserts nothing + * else, so a RED row is always a RED for the cause it states. The "control" + * groups are behaviour that is already correct (C argument guards, a fresh + * handle, a successful re-arm, a live stream snapshot) and must stay green + * under any fix. */ + +#define PX_TEXT 256 +#define PX_MAX_TRADES 8 +#define PX_MAX_POINTS 64 +#define PX_SENTINEL (-777) +#define PX_NOT_READY_TEXT "native run requires configure_native" +#define PX_SPEC_REJECTED_TEXT "native run spec rejected" +#define PX_KEY_CHANGED_TEXT "native session key cannot change on a reused host" +#define PX_INTRABAR_TEXT "native intrabar path failed validation" + +enum { PX_OUT_UNTOUCHED = 0, PX_OUT_EMPTY = 1, PX_OUT_ROWS = 2 }; +static const char* const px_out_name[3] = {"untouched", "empty", "rows"}; + +typedef struct px_trade { + int64_t entry_time; + int64_t exit_time; + double entry_price; + double exit_price; + double pnl; + double pnl_pct; + int is_long; + double max_runup; + double max_drawdown; + double qty; + double commission; + int32_t entry_bar_index; + int32_t exit_bar_index; + int32_t open_at_end; +} px_trade; + +typedef struct px_point { + int64_t time_ms; + double equity; + double open_profit; +} px_point; + +/* The defined fields of a pf_report_t, deep-copied out of its heap arrays. */ +typedef struct px_report { + int out_class; + int total_trades; + int trades_len; + double net_profit; + int64_t input_bars; + int64_t script_bars; + int input_tf_seconds; + int script_tf_seconds; + int script_tf_ratio; + int needs_aggregation; + int bar_magnifier_enabled; + int all_num_trades; + int64_t equity_len; + int64_t hash_len; + int trades_copied; + int points_copied; + int hashes_copied; + px_trade trades[PX_MAX_TRADES]; + px_point points[PX_MAX_POINTS]; + uint64_t hashes[PX_MAX_POINTS]; +} px_report; + +typedef struct px_state { + int rc; + uint32_t lifecycle; + uint32_t failure_code; + uint32_t failure_operation; + uint32_t failure_discriminator; + uint32_t completion; + uint64_t failure_ordinal; + uint64_t consumed_high_water; + int64_t decision_floor_ms; +} px_state; + +typedef struct px_attempt { + const char* tag; + int rc; + int last_status; + char error[PX_TEXT]; + char error_code[PX_TEXT]; + px_state st; + int hash_rc; + uint64_t hash; + px_report rep; +} px_attempt; + +typedef struct px_fixture { + pf_strategy_t host; + twin_state cb; + pf_native_run_spec_v1 spec; + const pf_bar_t* bars; + int n; + px_attempt first; +} px_fixture; + +static void px_expect(int ok, const char* scenario, const char* what) { + if (!ok) { + fprintf(stderr, "FAIL px %s: %s\n", scenario, what); + ++failures; + } +} + +static int px_cause(int reached, const char* scenario, const char* cause) { + printf("PXCAUSE scenario=%s cause=\"%s\" %s\n", scenario, cause, + reached ? "REACHED" : "NOT-REACHED"); + fflush(stdout); + if (!reached) { + fprintf(stderr, "FAIL px %s: refusal cause not reached (%s); no RED is claimed for this row\n", + scenario, cause); + ++failures; + } + return reached; +} + +static void px_copy_text(char* dst, size_t cap, const char* src) { + dst[0] = '\0'; + if (!src) return; + strncpy(dst, src, cap - 1); + dst[cap - 1] = '\0'; +} + +/* A report pre-filled so "untouched" is distinguishable from "zero-filled". */ +static void px_sentinel(pf_report_t* r) { + memset(r, 0, sizeof(*r)); + r->total_trades = PX_SENTINEL; + r->trades_len = PX_SENTINEL; + r->net_profit = (double)PX_SENTINEL; + r->input_bars_processed = PX_SENTINEL; + r->script_bars_processed = PX_SENTINEL; + r->equity_curve_len = PX_SENTINEL; + r->broker_state_hash_len = PX_SENTINEL; +} + +static int px_out_class(const pf_report_t* r) { + if (r->total_trades == PX_SENTINEL && r->trades_len == PX_SENTINEL + && r->net_profit == (double)PX_SENTINEL && r->input_bars_processed == PX_SENTINEL + && r->script_bars_processed == PX_SENTINEL && r->equity_curve_len == PX_SENTINEL + && r->broker_state_hash_len == PX_SENTINEL) { + return PX_OUT_UNTOUCHED; + } + if (r->total_trades == 0 && r->trades_len == 0 && r->equity_curve_len == 0 + && r->input_bars_processed == 0 && r->script_bars_processed == 0) { + return PX_OUT_EMPTY; + } + return PX_OUT_ROWS; +} + +static void px_release(pf_report_t* r) { + if (px_out_class(r) != PX_OUT_UNTOUCHED) strategy_native_report_free_v1(r); +} + +static void px_snapshot(px_report* d, const pf_report_t* s) { + int64_t i; + memset(d, 0, sizeof(*d)); + d->out_class = px_out_class(s); + d->total_trades = s->total_trades; + d->trades_len = s->trades_len; + d->net_profit = s->net_profit; + d->input_bars = s->input_bars_processed; + d->script_bars = s->script_bars_processed; + d->input_tf_seconds = s->input_tf_seconds; + d->script_tf_seconds = s->script_tf_seconds; + d->script_tf_ratio = s->script_tf_ratio; + d->needs_aggregation = s->needs_aggregation; + d->bar_magnifier_enabled = s->bar_magnifier_enabled; + d->all_num_trades = (int)s->metrics.all.num_trades; + d->equity_len = s->equity_curve_len; + d->hash_len = s->broker_state_hash_len; + if (s->trades) { + for (i = 0; i < (int64_t)s->trades_len && i < PX_MAX_TRADES; ++i) { + const pf_trade_t* t = &s->trades[i]; + px_trade* c = &d->trades[d->trades_copied++]; + c->entry_time = t->entry_time; + c->exit_time = t->exit_time; + c->entry_price = t->entry_price; + c->exit_price = t->exit_price; + c->pnl = t->pnl; + c->pnl_pct = t->pnl_pct; + c->is_long = t->is_long; + c->max_runup = t->max_runup; + c->max_drawdown = t->max_drawdown; + c->qty = t->qty; + c->commission = t->commission; + c->entry_bar_index = t->entry_bar_index; + c->exit_bar_index = t->exit_bar_index; + c->open_at_end = t->open_at_end; + } + } + if (s->equity_curve) { + for (i = 0; i < s->equity_curve_len && i < PX_MAX_POINTS; ++i) { + px_point* c = &d->points[d->points_copied++]; + c->time_ms = s->equity_curve[i].time_ms; + c->equity = s->equity_curve[i].equity; + c->open_profit = s->equity_curve[i].open_profit; + } + } + if (s->broker_state_hash) { + for (i = 0; i < s->broker_state_hash_len && i < PX_MAX_POINTS; ++i) { + d->hashes[d->hashes_copied++] = s->broker_state_hash[i]; + } + } +} + +static int px_same_double(double a, double b) { + return a == b || (isnan(a) && isnan(b)); +} + +static int px_trade_equal(const px_trade* a, const px_trade* b) { + return a->entry_time == b->entry_time && a->exit_time == b->exit_time + && px_same_double(a->entry_price, b->entry_price) + && px_same_double(a->exit_price, b->exit_price) + && px_same_double(a->pnl, b->pnl) && px_same_double(a->pnl_pct, b->pnl_pct) + && a->is_long == b->is_long + && px_same_double(a->max_runup, b->max_runup) + && px_same_double(a->max_drawdown, b->max_drawdown) + && px_same_double(a->qty, b->qty) && px_same_double(a->commission, b->commission) + && a->entry_bar_index == b->entry_bar_index && a->exit_bar_index == b->exit_bar_index + && a->open_at_end == b->open_at_end; +} + +/* Field-by-field equality of two deep copies. include_hash = 0 leaves the + * per-bar continuation hashes out (they fold the run identity, so two runs + * under different run numbers legitimately differ there). */ +static int px_report_equal(const px_report* a, const px_report* b, int include_hash) { + int i; + if (a->total_trades != b->total_trades || a->trades_len != b->trades_len + || !px_same_double(a->net_profit, b->net_profit) + || a->input_bars != b->input_bars || a->script_bars != b->script_bars + || a->input_tf_seconds != b->input_tf_seconds + || a->script_tf_seconds != b->script_tf_seconds + || a->script_tf_ratio != b->script_tf_ratio + || a->needs_aggregation != b->needs_aggregation + || a->bar_magnifier_enabled != b->bar_magnifier_enabled + || a->all_num_trades != b->all_num_trades || a->equity_len != b->equity_len + || a->trades_copied != b->trades_copied || a->points_copied != b->points_copied) { + return 0; + } + for (i = 0; i < a->trades_copied; ++i) { + if (!px_trade_equal(&a->trades[i], &b->trades[i])) return 0; + } + for (i = 0; i < a->points_copied; ++i) { + if (a->points[i].time_ms != b->points[i].time_ms + || !px_same_double(a->points[i].equity, b->points[i].equity) + || !px_same_double(a->points[i].open_profit, b->points[i].open_profit)) { + return 0; + } + } + if (include_hash) { + if (a->hash_len != b->hash_len || a->hashes_copied != b->hashes_copied) return 0; + for (i = 0; i < a->hashes_copied; ++i) { + if (a->hashes[i] != b->hashes[i]) return 0; + } + } + return 1; +} + +static void px_read_state(px_state* s, pf_strategy_t host) { + pf_native_state_v1 raw; + memset(s, 0, sizeof(*s)); + memset(&raw, 0, sizeof(raw)); + raw.struct_size = (uint32_t)sizeof(raw); + s->rc = strategy_native_state_v1(host, &raw); + if (s->rc != PF_NATIVE_OK) return; + s->lifecycle = raw.lifecycle; + s->failure_code = raw.failure_code; + s->failure_operation = raw.failure_operation; + s->failure_discriminator = raw.failure_discriminator; + s->completion = raw.completion; + s->failure_ordinal = raw.failure_ordinal; + s->consumed_high_water = raw.consumed_high_water; + s->decision_floor_ms = raw.decision_floor_ms; +} + +/* Reads the presentation channels first: a later accessor may clear them. */ +static void px_observe(px_attempt* a, const char* tag, pf_strategy_t host, int rc, + const pf_report_t* out) { + memset(a, 0, sizeof(*a)); + a->tag = tag; + a->rc = rc; + a->last_status = strategy_last_run_status(host); + px_copy_text(a->error, sizeof(a->error), strategy_get_last_error(host)); + px_copy_text(a->error_code, sizeof(a->error_code), strategy_get_last_error_code(host)); + px_read_state(&a->st, host); + a->hash_rc = strategy_native_continuation_hash_v1(host, &a->hash); + px_snapshot(&a->rep, out); +} + +static void px_print(const px_attempt* a) { + printf("PXREC tag=%s rc=%d last_status=%d error=\"%s\" error_code=\"%s\" state_rc=%d " + "lifecycle=%u failure=%u op=%u disc=%u completion=%u hwm=%llu hash_rc=%d hash=%016llx " + "out=%s total_trades=%d trades_len=%d net_profit=%.17g input_bars=%lld " + "script_bars=%lld equity_len=%lld hash_len=%lld\n", + a->tag, a->rc, a->last_status, a->error, a->error_code, a->st.rc, + (unsigned)a->st.lifecycle, (unsigned)a->st.failure_code, + (unsigned)a->st.failure_operation, (unsigned)a->st.failure_discriminator, + (unsigned)a->st.completion, (unsigned long long)a->st.consumed_high_water, + a->hash_rc, (unsigned long long)a->hash, px_out_name[a->rep.out_class], + a->rep.total_trades, a->rep.trades_len, a->rep.net_profit, + (long long)a->rep.input_bars, (long long)a->rep.script_bars, + (long long)a->rep.equity_len, (long long)a->rep.hash_len); + fflush(stdout); +} + +static void px_print_configure(const char* tag, pf_strategy_t host, int rc, uint32_t error, + uint32_t field) { + char text[PX_TEXT]; + px_state st; + const int status = strategy_last_run_status(host); + px_copy_text(text, sizeof(text), strategy_get_last_error(host)); + px_read_state(&st, host); + printf("PXCFG tag=%s rc=%d error=%u field=%u last_status=%d last_error=\"%s\" " + "lifecycle=%u failure=%u hwm=%llu\n", + tag, rc, (unsigned)error, (unsigned)field, status, text, (unsigned)st.lifecycle, + (unsigned)st.failure_code, (unsigned long long)st.consumed_high_water); + fflush(stdout); +} + +/* One clean first run on a fresh handle: the setup of every refused-attempt + * row, and itself the "fresh handle" positive control. */ +static int px_open(px_fixture* f, const char* scenario, const char* session_key) { + pf_native_callbacks_v1 table; + pf_report_t out; + int rc; + memset(f, 0, sizeof(*f)); + f->spec = twin_spec(); + f->spec.session_key = session_key; + table = blank_callbacks(&f->cb); + table.on_bar = twin_on_bar; + f->host = strategy_native_host_create_v1(&table); + if (!f->host) { + px_expect(0, scenario, "host create failed"); + return 0; + } + f->cb.host = f->host; + f->bars = pf_twin_bars(&f->n); + if (strategy_configure_native_v1(f->host, &f->spec) != 0) { + px_expect(0, scenario, "first configure refused"); + strategy_native_host_free(f->host); + f->host = NULL; + return 0; + } + px_sentinel(&out); + rc = strategy_native_run_v1(f->host, f->bars, f->n, &out); + px_observe(&f->first, "first-run", f->host, rc, &out); + px_release(&out); + printf("PXSETUP scenario=%s\n", scenario); + px_print(&f->first); + if (!px_cause(f->first.rc == PF_NATIVE_OK && f->first.last_status == 0 + && f->first.error[0] == '\0' + && f->first.st.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED + && f->first.st.consumed_high_water == 1u + && f->first.rep.out_class == PX_OUT_ROWS && f->first.rep.trades_copied >= 1, + scenario, "the first run completed cleanly with rows that could go stale")) { + strategy_native_host_free(f->host); + f->host = NULL; + return 0; + } + return 1; +} + +/* What every refused SECOND attempt must answer. The cause check comes first + * and gates the rest. A Failed host keeps the text of the refusal that latched + * it (the run call stops at prepare_public_begin), so for the latch rows the + * retained text names the settings cause. status_channel = 1 asserts that + * last_run_status records the refusal (true on every row measured). */ +static void px_check_refused(const char* scenario, const px_fixture* f, const px_attempt* a, + const char* cause_text, int cause_is_prefix, uint32_t lifecycle, + uint32_t failure_code, int status_channel) { + const size_t len = strlen(cause_text); + const int text_ok = cause_is_prefix ? strncmp(a->error, cause_text, len) == 0 + : strcmp(a->error, cause_text) == 0; + if (!px_cause(text_ok && a->st.rc == PF_NATIVE_OK && a->st.lifecycle == lifecycle + && a->st.failure_code == failure_code, + scenario, cause_text)) { + return; + } + if (status_channel) { + px_expect(a->last_status != 0, scenario, + "last_run_status does not record the refused attempt"); + } + /* The return value is this attempt's status, not the handle's lifecycle. */ + px_expect(a->rc != PF_NATIVE_OK && a->rc < 0, scenario, + "refused attempt did not return a failure status (current-attempt status)"); + /* The report is this attempt's, never an earlier attempt's. */ + px_expect(a->rep.out_class != PX_OUT_ROWS, scenario, + "refused attempt published report rows (no-report)"); + px_expect(!(a->rep.out_class == PX_OUT_ROWS && px_report_equal(&a->rep, &f->first.rep, 1)), + scenario, "refused attempt published the previous run's report, every defined field equal"); +} + +/* ── Controls: behaviour that is already correct and must stay so ─── */ + +static void px_control_rearm(void) { + const char* scenario = "control-successful-rearm"; + px_fixture f; + px_attempt a; + pf_report_t out; + pf_native_run_spec_v1 next; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-control-rearm")) return; + next = f.spec; + next.run_number = 2; + rc = strategy_configure_native_v1(f.host, &next); + px_expect(rc == 0, scenario, "the re-arm configure was refused"); + f.cb.calculations = 0; + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "control-rearm.second-run", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_expect(a.rc == PF_NATIVE_OK && a.last_status == 0 && a.error[0] == '\0', scenario, + "a re-armed run is not a clean success"); + px_expect(a.st.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED && a.st.consumed_high_water == 2u, + scenario, "a re-armed run did not complete as run 2"); + px_expect(a.rep.out_class == PX_OUT_ROWS && px_report_equal(&a.rep, &f.first.rep, 0), scenario, + "a re-armed run's report differs from the first run's (same bars, same callbacks)"); + strategy_native_host_free(f.host); +} + +static void px_control_c_guards(void) { + const char* scenario = "control-null-handle-and-fresh-handle"; + pf_native_callbacks_v1 table = blank_callbacks(NULL); + pf_report_t out; + px_attempt a; + pf_strategy_t host; + const pf_bar_t* bars; + int n = 0; + int rc; + bars = pf_twin_bars(&n); + px_sentinel(&out); + rc = strategy_native_run_v1(NULL, bars, n, &out); + px_expect(rc == PF_NATIVE_E_HANDLE && px_out_class(&out) == PX_OUT_UNTOUCHED, scenario, + "a NULL handle was not refused with E_HANDLE and an untouched report"); + host = strategy_native_host_create_v1(&table); + px_expect(host != NULL, scenario, "fresh host create failed"); + if (!host) return; + px_sentinel(&out); + rc = strategy_native_run_v1(host, bars, n, &out); + px_observe(&a, "control-fresh-unconfigured.run", host, rc, &out); + px_release(&out); + px_print(&a); + px_expect(a.rc == PF_NATIVE_E_RUN_FAILED, scenario, + "a never-configured handle did not answer E_RUN_FAILED"); + px_expect(a.st.lifecycle == PF_NATIVE_LIFECYCLE_UNCONFIGURED, scenario, + "a never-configured handle left Unconfigured"); + px_expect(a.rep.out_class != PX_OUT_ROWS, scenario, + "a never-configured handle published report rows"); + strategy_native_host_free(host); +} + +static void px_control_stream_snapshot(void) { + const char* scenario = "control-live-stream-snapshot"; + px_fixture f; + px_attempt mid; + px_attempt end; + pf_native_callbacks_v1 table; + pf_report_t out; + int rc; + int i; + memset(&f, 0, sizeof(f)); + f.spec = twin_spec(); + f.spec.session_key = "px-native-attempt-control-stream"; + table = blank_callbacks(&f.cb); + table.on_bar = twin_on_bar; + f.host = strategy_native_host_create_v1(&table); + px_expect(f.host != NULL, scenario, "stream host create failed"); + if (!f.host) return; + f.cb.host = f.host; + f.bars = pf_twin_bars(&f.n); + px_expect(strategy_configure_native_v1(f.host, &f.spec) == 0, scenario, "stream configure refused"); + px_expect(strategy_stream_begin(f.host, f.bars, 4, "5", "5") == 0, scenario, + "the stream warmup did not begin"); + for (i = 4; i < 24; ++i) { + if (strategy_stream_push_bar(f.host, &f.bars[i]) != 0) { + px_expect(0, scenario, "a live bar was refused"); + break; + } + } + px_sentinel(&out); + rc = strategy_stream_fill_report(f.host, &out); + px_observe(&mid, "control-stream.mid-snapshot", f.host, rc, &out); + px_release(&out); + px_print(&mid); + px_expect(mid.rc == 0 && mid.rep.out_class == PX_OUT_ROWS && mid.rep.script_bars > 0, + scenario, "a mid-stream snapshot did not return the stream's own rows"); + for (i = 24; i < f.n; ++i) { + if (strategy_stream_push_bar(f.host, &f.bars[i]) != 0) { + px_expect(0, scenario, "a later live bar was refused"); + break; + } + } + px_expect(strategy_stream_end(f.host, 0) == 0, scenario, "the stream did not end"); + px_sentinel(&out); + rc = strategy_stream_fill_report(f.host, &out); + px_observe(&end, "control-stream.final-snapshot", f.host, rc, &out); + px_release(&out); + px_print(&end); + px_expect(end.rc == 0 && end.rep.out_class == PX_OUT_ROWS + && end.rep.script_bars >= mid.rep.script_bars, + scenario, "the final stream snapshot did not extend the mid-stream one"); + strategy_native_host_free(f.host); +} + +/* ── Refused second attempts, one scenario per cause ──────────────── */ + +static void px_refused_lifecycle(void) { + const char* scenario = "refused-lifecycle-no-rearm"; + px_fixture f; + px_attempt a; + pf_report_t out; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-lifecycle")) return; + /* Negative control: the C argument guard answers before the kernel, even on + * a Completed handle, and never fills a report. */ + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, NULL, f.n, &out); + px_observe(&a, "control-c-guard.completed-null-bars", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_expect(a.rc == PF_NATIVE_E_ARGUMENT && a.rep.out_class == PX_OUT_UNTOUCHED + && a.st.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED, + scenario, "NULL bars on a Completed handle was not refused by the C guard before any report"); + /* A bad feed on a Completed handle is refused as "not Ready" before the bar + * preflight runs: that is lifecycle coverage, recorded here, never feed + * coverage (the feed rows re-arm the handle first). */ + { + pf_bar_t bad_bars[TWIN_BARS]; + memcpy(bad_bars, f.bars, sizeof(bad_bars)); + bad_bars[20].timestamp = bad_bars[19].timestamp; + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, bad_bars, f.n, &out); + px_observe(&a, "lifecycle.bad-feed-on-completed-handle(not-feed-coverage)", f.host, rc, &out); + px_release(&out); + px_print(&a); + } + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "lifecycle.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_expect(a.st.consumed_high_water == f.first.st.consumed_high_water, scenario, + "the refused second call moved the consumed high-water mark"); + px_check_refused(scenario, &f, &a, PX_NOT_READY_TEXT, 0, PF_NATIVE_LIFECYCLE_COMPLETED, + PF_NATIVE_FAILURE_NONE, 1); + strategy_native_host_free(f.host); +} + +static void px_refused_settings_typed(void) { + const char* scenario = "refused-bad-settings-typed-refusal"; + px_fixture f; + px_attempt a; + pf_report_t out; + pf_native_run_spec_v1 bad; + pf_native_run_spec_ext_v1 ext; + uint32_t error = TYPED_UNTOUCHED; + uint32_t field = TYPED_UNTOUCHED; + px_state st; + int cfg; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-settings-typed")) return; + bad = f.spec; + bad.run_number = 2; + bad.initial_capital = 0.0; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + cfg = strategy_configure_native_ext_result_v1(f.host, &bad, &ext, &error, &field); + px_print_configure("bad-settings-typed.configure", f.host, cfg, error, field); + px_read_state(&st, f.host); + if (!px_cause(cfg == PF_NATIVE_E_ARGUMENT && error == PF_NATIVE_SPEC_ERROR_NOT_FINITE_POSITIVE + && field == PF_NATIVE_SPEC_FIELD_INITIAL_CAPITAL + && st.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED, + scenario, "typed settings refusal named initial_capital and left the handle Completed")) { + strategy_native_host_free(f.host); + return; + } + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "bad-settings-typed.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_check_refused(scenario, &f, &a, PX_NOT_READY_TEXT, 0, PF_NATIVE_LIFECYCLE_COMPLETED, + PF_NATIVE_FAILURE_NONE, 1); + strategy_native_host_free(f.host); +} + +static void px_refused_settings_latch(void) { + const char* scenario = "refused-bad-settings-invalid-spec-latch"; + px_fixture f; + px_attempt a; + pf_report_t out; + pf_native_run_spec_v1 bad; + px_state st; + int cfg; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-settings-latch")) return; + bad = f.spec; + bad.run_number = 2; + bad.initial_capital = 0.0; + cfg = strategy_configure_native_v1(f.host, &bad); + px_print_configure("bad-settings-latch.configure", f.host, cfg, 0u, 0u); + px_read_state(&st, f.host); + if (!px_cause(cfg == -1 && st.lifecycle == PF_NATIVE_LIFECYCLE_FAILED + && st.failure_code == PF_NATIVE_FAILURE_INVALID_SPECIFICATION, + scenario, "legacy configure of an invalid spec latched Failed(InvalidSpecification)")) { + strategy_native_host_free(f.host); + return; + } + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "bad-settings-latch.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_check_refused(scenario, &f, &a, PX_SPEC_REJECTED_TEXT, 0, PF_NATIVE_LIFECYCLE_FAILED, + PF_NATIVE_FAILURE_INVALID_SPECIFICATION, 1); + strategy_native_host_free(f.host); +} + +static void px_refused_settings_reuse(void) { + const char* scenario = "refused-bad-settings-reused-session-key"; + px_fixture f; + px_attempt a; + pf_report_t out; + pf_native_run_spec_v1 changed; + pf_native_run_spec_ext_v1 ext; + uint32_t error = TYPED_UNTOUCHED; + uint32_t field = TYPED_UNTOUCHED; + px_state st; + int cfg; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-settings-reuse")) return; + changed = f.spec; + changed.session_key = "px-native-attempt-settings-reuse-changed"; + changed.run_number = 2; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + cfg = strategy_configure_native_ext_result_v1(f.host, &changed, &ext, &error, &field); + px_print_configure("bad-settings-reuse.configure", f.host, cfg, error, field); + px_read_state(&st, f.host); + if (!px_cause(cfg == PF_NATIVE_E_ARGUMENT + && error == PF_NATIVE_SPEC_ERROR_SESSION_KEY_CHANGED_ON_REUSE + && field == PF_NATIVE_SPEC_FIELD_SESSION_KEY + && st.lifecycle == PF_NATIVE_LIFECYCLE_FAILED + && st.failure_code == PF_NATIVE_FAILURE_CONTRACT, + scenario, "a reused host's changed session key latched Failed(Contract)")) { + strategy_native_host_free(f.host); + return; + } + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "bad-settings-reuse.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_check_refused(scenario, &f, &a, PX_KEY_CHANGED_TEXT, 0, PF_NATIVE_LIFECYCLE_FAILED, + PF_NATIVE_FAILURE_CONTRACT, 1); + strategy_native_host_free(f.host); +} + +/* The feed cause. The handle is re-armed FIRST, so the refusal that answers is + * the bar preflight's; the C argument guards on the same Ready handle are the + * negative controls (they refuse before the kernel and never fill a report). */ +static void px_refused_feed(void) { + const char* scenario = "refused-bad-feed-after-rearm"; + px_fixture f; + px_attempt a; + px_attempt guard; + pf_report_t out; + pf_native_run_spec_v1 next; + pf_bar_t bad_bars[TWIN_BARS]; + px_state st; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-feed")) return; + next = f.spec; + next.run_number = 2; + rc = strategy_configure_native_v1(f.host, &next); + px_read_state(&st, f.host); + if (!px_cause(rc == 0 && st.lifecycle == PF_NATIVE_LIFECYCLE_READY, scenario, + "the handle was re-armed to Ready before the bad feed")) { + strategy_native_host_free(f.host); + return; + } + + /* Negative controls: the C argument guards, already correct. */ + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, NULL, f.n, &out); + px_observe(&guard, "control-c-guard.null-bars", f.host, rc, &out); + px_release(&out); + px_print(&guard); + px_expect(guard.rc == PF_NATIVE_E_ARGUMENT && guard.rep.out_class == PX_OUT_UNTOUCHED + && guard.st.lifecycle == PF_NATIVE_LIFECYCLE_READY, + scenario, "NULL bars with n > 0 was not refused by the C guard before any report"); + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, -1, &out); + px_observe(&guard, "control-c-guard.negative-count", f.host, rc, &out); + px_release(&out); + px_print(&guard); + px_expect(guard.rc == PF_NATIVE_E_ARGUMENT && guard.rep.out_class == PX_OUT_UNTOUCHED + && guard.st.lifecycle == PF_NATIVE_LIFECYCLE_READY, + scenario, "a negative count was not refused by the C guard before any report"); + + /* The feed refusal: a repeated timestamp, reached through preflight_bars. */ + memcpy(bad_bars, f.bars, sizeof(bad_bars)); + bad_bars[20].timestamp = bad_bars[19].timestamp; + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, bad_bars, f.n, &out); + px_observe(&a, "bad-feed.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_check_refused(scenario, &f, &a, "bar[", 1, PF_NATIVE_LIFECYCLE_READY, + PF_NATIVE_FAILURE_NONE, 1); + + /* Refusals do not consume the arm: the same Ready handle then runs clean. */ + f.cb.calculations = 0; + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&guard, "control-rearm.valid-run-after-refusals", f.host, rc, &out); + px_release(&out); + px_print(&guard); + px_expect(guard.rc == PF_NATIVE_OK && guard.last_status == 0 && guard.error[0] == '\0' + && guard.st.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED + && guard.st.consumed_high_water == 2u + && px_report_equal(&guard.rep, &f.first.rep, 0), + scenario, "the valid run after the refusals was not a clean run 2"); + strategy_native_host_free(f.host); +} + +/* The capability cause: a retained intrabar path whose lower feed is refused by + * the run-time preflight, on a handle that was re-armed with it. */ +static pf_bar_t px_bad_lower[TWIN_BARS * SUB_PER_BAR]; + +static void px_refused_capability(void) { + const char* scenario = "refused-capability-intrabar-path-after-rearm"; + px_fixture f; + px_attempt a; + pf_report_t out; + pf_native_run_spec_v1 next; + pf_native_run_spec_ext_v1 ext; + const pf_bar_t* lower; + uint32_t error = TYPED_UNTOUCHED; + uint32_t field = TYPED_UNTOUCHED; + px_state st; + int lower_n = 0; + int cfg; + int rc; + if (!px_open(&f, scenario, "px-native-attempt-capability")) return; + lower = lower_feed(&lower_n); + memcpy(px_bad_lower, lower, sizeof(px_bad_lower)); + px_bad_lower[3].timestamp = px_bad_lower[2].timestamp; + next = f.spec; + next.run_number = 2; + memset(&ext, 0, sizeof(ext)); + ext.struct_size = (uint32_t)sizeof(ext); + ext.version = PF_NATIVE_API_VERSION; + ext.present_mask = PF_NATIVE_SPEC_EXT_INTRABAR; + ext.intrabar_kind = PF_NATIVE_INTRABAR_LOWER_TF; + ext.intrabar_tf = "1"; + ext.intrabar_bars = px_bad_lower; + ext.intrabar_n = lower_n; + ext.intrabar_samples = 4; + ext.intrabar_distribution = PF_MAGNIFIER_ENDPOINTS; + ext.intrabar_volume_weighted = 0u; + ext.intrabar_volume_weighted_min_samples = 2; + ext.intrabar_volume_weighted_max_samples = 64; + ext.intrabar_sample_eligibility = PF_NATIVE_SAMPLE_CONTINUOUS_SEGMENTS; + cfg = strategy_configure_native_ext_result_v1(f.host, &next, &ext, &error, &field); + px_print_configure("capability.configure", f.host, cfg, error, field); + px_read_state(&st, f.host); + if (!px_cause(cfg == PF_NATIVE_OK && st.lifecycle == PF_NATIVE_LIFECYCLE_READY, scenario, + "the handle was re-armed to Ready with an intrabar path whose lower feed is bad")) { + strategy_native_host_free(f.host); + return; + } + px_sentinel(&out); + rc = strategy_native_run_v1(f.host, f.bars, f.n, &out); + px_observe(&a, "capability.second-call", f.host, rc, &out); + px_release(&out); + px_print(&a); + px_check_refused(scenario, &f, &a, PX_INTRABAR_TEXT, 0, PF_NATIVE_LIFECYCLE_READY, + PF_NATIVE_FAILURE_NONE, 1); + strategy_native_host_free(f.host); +} + +static void check_px_native_attempt(void) { + printf("PXSUITE begin\n"); + px_control_rearm(); + px_control_c_guards(); + px_control_stream_snapshot(); + px_refused_lifecycle(); + px_refused_settings_typed(); + px_refused_settings_latch(); + px_refused_settings_reuse(); + px_refused_feed(); + px_refused_capability(); + printf("PXSUITE end\n"); + fflush(stdout); +} + +/* A genuine host that produces a trade and then fails: the twin arm's entry + * and flatten close one round trip, then the callback refuses five + * calculations after the exit. With a non-NULL out the failed call keeps its + * own partial report (the closed trade and the bar counters of the run that + * began, as before the attempt gate), and the next call on the Failed handle is + * refused without beginning: E_RUN_FAILED and the empty report, never the failed + * run's rows. Defined fields only. */ +static int partial_trade_then_fail_on_bar(void* user, const pf_bar_t* bar, + const pf_native_decision_v1* at) { + twin_state* state = (twin_state*)user; + const int rc = twin_on_bar(user, bar, at); + if (rc != 0) return rc; + return state->calculations == PF_TWIN_EXIT_BAR + 5 ? 7 : 0; +} + +static void check_started_then_failed_report(void) { + pf_native_run_spec_v1 spec = twin_spec(); + pf_native_callbacks_v1 table; + pf_native_state_v1 native_state; + twin_state state; + pf_report_t report; + pf_report_t reuse; + const pf_bar_t* bars; + int n = 0; + + memset(&state, 0, sizeof(state)); + memset(&report, 0, sizeof(report)); + table = blank_callbacks(&state); + table.on_bar = partial_trade_then_fail_on_bar; + state.host = strategy_native_host_create_v1(&table); + CHECK(state.host != NULL, "partial-report host create failed"); + if (!state.host) return; + CHECK_EQ_INT(strategy_configure_native_v1(state.host, &spec), 0, "partial-report configure"); + bars = pf_twin_bars(&n); + CHECK(n > PF_TWIN_EXIT_BAR + 5, "the twin bars end before the failing calculation"); + CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, n, &report), PF_NATIVE_E_RUN_FAILED, + "a callback failure after a trade did not fail the run"); + memset(&native_state, 0, sizeof(native_state)); + native_state.struct_size = (uint32_t)sizeof(native_state); + CHECK_EQ_INT(strategy_native_state_v1(state.host, &native_state), PF_NATIVE_OK, + "failed-state read"); + CHECK_EQ_INT(native_state.lifecycle, PF_NATIVE_LIFECYCLE_FAILED, "lifecycle is not Failed"); + CHECK_EQ_INT(state.command_error, 0, "a command returned an error before the failure"); + CHECK_EQ_INT(report.total_trades, 1, "the partial report lost the closed trade"); + CHECK_EQ_INT(report.trades_len, 1, "the partial report lost the trade row"); + CHECK(report.trades != NULL, "the partial report has no trade array"); + CHECK(report.script_bars_processed > PF_TWIN_EXIT_BAR, + "the partial report lost the bar counters"); + strategy_native_report_free_v1(&report); + + /* Indeterminate bytes in, the empty report out: overwritten, never freed. */ + memset(&reuse, 0x5a, sizeof(reuse)); + CHECK_EQ_INT(strategy_native_run_v1(state.host, bars, n, &reuse), PF_NATIVE_E_RUN_FAILED, + "a Failed handle ran again"); + CHECK_EQ_INT(reuse.total_trades, 0, "the refused call kept the failed run's trade count"); + CHECK_EQ_INT(reuse.trades_len, 0, "the refused call kept the failed run's trade rows"); + CHECK(reuse.trades == NULL, "the refused call handed out the failed run's trade array"); + CHECK_EQ_INT(reuse.script_bars_processed, 0, + "the refused call kept the failed run's bar counters"); + strategy_native_report_free_v1(&reuse); + strategy_native_host_free(state.host); +} + int pf_native_c_api_checks(void) { failures = 0; check_csurface_create_and_configure_refusals(); @@ -9348,6 +10232,7 @@ int pf_native_c_api_checks(void) { check_spec_extension(); check_lifecycle_round_trips(); check_callback_failure_latch(); + check_started_then_failed_report(); check_event_polling(); check_event_retention(); check_unrepresentable_quantity(); @@ -9393,5 +10278,6 @@ int pf_native_c_api_checks(void) { check_decision_session_day_of_a_masked_month(); check_working_relation_tail(); check_session_day_tail(); + check_px_native_attempt(); return failures; } diff --git a/tests/test_native_example_batch.cpp b/tests/test_native_example_batch.cpp index bc45826db..46690625e 100644 --- a/tests/test_native_example_batch.cpp +++ b/tests/test_native_example_batch.cpp @@ -468,6 +468,37 @@ int main(int argc, char** argv) { abi.free_strategy(s); } + { + // A module call the engine refuses without beginning (nothing re-armed + // a Completed handle) hands out the empty report, never the first + // run's rows: the shared engine report gate, not a wrapper policy. The + // handle keeps its Completed lifecycle and runs again once re-armed. + pf_strategy_t s = abi.create(nullptr); + CHECK(s != nullptr); + auto spec = complete_spec("native-example-refused-rerun", 1); + CHECK(abi.configure(s, &spec) == 0); + pf_report_t first{}; + abi.run_full(s, bars, 5, "5", "5", 0, 4, PF_MAGNIFIER_ENDPOINTS, &first); + expect_closed_round_trip(first); + abi.report_free(&first); + pf_report_t refused{}; + abi.run_full(s, bars, 5, "5", "5", 0, 4, PF_MAGNIFIER_ENDPOINTS, &refused); + const char* refusal = abi.last_error(s); + CHECK(refusal != nullptr && refusal[0] != '\0'); + CHECK(abi.last_status(s) == 1); + CHECK(refused.trades == nullptr); + CHECK(refused.trades_len == 0); + CHECK(refused.total_trades == 0); + abi.report_free(&refused); + spec = complete_spec("native-example-refused-rerun", 2); + CHECK(abi.configure(s, &spec) == 0); + pf_report_t rearmed{}; + abi.run_full(s, bars, 5, "5", "5", 0, 4, PF_MAGNIFIER_ENDPOINTS, &rearmed); + expect_closed_round_trip(rearmed); + abi.report_free(&rearmed); + abi.free_strategy(s); + } + { // Wrapper preflight preserves the output report on unsupported magnifier // arguments. Does not latch Failed; Ready remains usable. diff --git a/tests/test_run_failure_codes.cpp b/tests/test_run_failure_codes.cpp index 53d863386..f034635d1 100644 --- a/tests/test_run_failure_codes.cpp +++ b/tests/test_run_failure_codes.cpp @@ -491,7 +491,7 @@ void record_lifecycle_rows(const std::vector& minute) { h.input("Case", "0"); expect_row("reuse_failed_handle_refused", h.run(minute, "1", "1"), {"engine_invariant", "{}", - "Pine native adapter failed to configure projected run spec", 1, 1}); + "Pine native adapter failed to configure projected run spec", 1, 0}); } { // The generated wrapper's contract (note_run_failure with an entrypoint). From 1c89e87a5e9a71a353125128dc34e44debccb8c8 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 13:28:42 +0800 Subject: [PATCH 2/4] Close the report-ownership review findings: pin, nested and snapshot controls, guide wording Follow-up to 44f7d0f8e (Deliver a native run's report only for the call that began it, onto #356), which integrated the 3d2f3f4 .. d83ab7d4a range onto main 70ddaa3a7. This commit answers the final review of that work (one P1, two P2) and changes no source under src/ or include/, no PF_API export and no admission behavior. P1, documentation content pin. The stream-report call in strategy_stream_fill_report (src/c_abi.cpp:770) sits inside the window c_abi.cpp:308-963 that docs/design/native-feature-parity.md row 8 pins by content, and a line-neutral edit does not preserve a content hash. The row's claim was re-read against the integrated source first: 73 PF_API definitions in c_abi.cpp, the 12-symbol stream family (strategy_stream_begin .. strategy_stream_fill_report), no export added or removed. The pin is refreshed from sha256:d19d72e6... to sha256:70be189a... by hashing the window's lines joined with newlines. All 15 content pins of the published pages were recomputed by text; 15 of 15 now match. P2, controls. tests/test_native_c_api.c gains check_nested_run_report_ownership: a genuine C callback, after a closed trade, makes a second strategy_native_run_v1 on its own handle over a poisoned output. The nested call must answer PF_NATIVE_E_RUN_FAILED with the empty report, while the outer run ends Failed (Contract, read from the state, not assumed Running) and publishes its own partial trade. tests/test_checked_settings.cpp gains test_snapshot_restores_batch_gate: after a batch call refused over a live stream, a normal and a throwing (injected allocation failure) strategy_stream_fill_report are followed by BacktestEngine::fill_report with no run in between, which must still answer empty. Both files only gain lines: the 14 reproduction assertions, the old, twin, live and started-then-failed controls are untouched. P2, guide. docs/pages/native-engine.md "A run's report." no longer says every listed refusal leaves the lifecycle, the run identity and the retained rows as it found them. It states the cause-by-cause behavior that exists unchanged on the base: a begin on a Completed, Failed or unconfigured handle and a refused bar array or run option preserve them; a calendar or timezone refusal comes after the consumed run-number high-water moved and fails the lifecycle; a begin while Running latches Failed (Contract, Begin; Contract, Configure on a generated source host). It also states that the batch reader stays empty after a snapshot and that a nested run call reads empty while the outer run keeps its rows. This rewords sentences introduced by 44f7d0f8e (and before it by 3d2f3f4, 86a1856, ec9047d): the dropped sentence "leaves the lifecycle, the run identity and the retained rows as it found them" is replaced, not restored. Not built, not run, not guard-checked here (no compiler or project helper on this lane). Focused GREEN and the integrated-tree proof are owed on a spot box. Co-Authored-By: Claude Sonnet 5.5 --- docs/design/native-feature-parity.md | 2 +- docs/pages/native-engine.md | 34 +++++++---- tests/test_checked_settings.cpp | 71 ++++++++++++++++++++++ tests/test_native_c_api.c | 89 ++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 12 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index ca6f539f6..3efc3d6ac 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -327,7 +327,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | | 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:489-507 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2512-2542; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | -| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 73 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 73 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:d19d72e63bbaf66ac67cb2e5180aaeb89dd0e756a5ea8c4d43ea43b737d1afbd` c_abi.cpp:308-963, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:20-80, c_abi.cpp:896, `sha256:3b43bbaa40f1bab3c7ffa4e20d45fd8ea2ba321e8a54b7cbe45dc625dfca59f6` c_abi.cpp:624-743, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:549-601, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:528-545, `sha256:2cb41f4006caae992ee41605054a3397689ae00bf3080011db1bba6412ffcd87` c_abi.cpp:836-867, c_abi.cpp:963 | F O S | +| 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 73 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 73 compiled-strategy runtime symbols in `c_abi.cpp` are distinct from the native C host API in `native_c_host.cpp`: the latter exposes submit, replace, cancel and a callback table to a C host. | `sha256:70be189ab83fc6c7586c9f647c39fbf76a4f2dfac09627039b730dcb3084d175` c_abi.cpp:308-963, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:20-80, c_abi.cpp:896, `sha256:3b43bbaa40f1bab3c7ffa4e20d45fd8ea2ba321e8a54b7cbe45dc625dfca59f6` c_abi.cpp:624-743, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:549-601, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:528-545, `sha256:2cb41f4006caae992ee41605054a3397689ae00bf3080011db1bba6412ffcd87` c_abi.cpp:836-867, c_abi.cpp:963 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:114 and `add_library` CMakeLists.txt:155 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:155 | S (F, O agree in §2.i) | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index e180ced9a..2d884c6fb 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4006,20 +4006,32 @@ rows of the last run it began, so `BacktestEngine::fill_report` — the one read 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 it refused without beginning (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, and leaves the lifecycle, the run -identity and the retained rows as it found them. 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 +(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 refusal comes after the consumed run-number +high-water moved, and it fails the lifecycle. A begin while the handle is +`Running` (a live stream, or a call 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 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. +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` diff --git a/tests/test_checked_settings.cpp b/tests/test_checked_settings.cpp index daad09c45..831dc6eb9 100644 --- a/tests/test_checked_settings.cpp +++ b/tests/test_checked_settings.cpp @@ -214,6 +214,76 @@ void test_refused_batch_over_live_stream() { std::cout << "batch refused over a live stream: empty batch report, stream snapshot keeps its rows PASS\n"; } +// The explicit snapshot hands the batch gate back as it found it. A batch call +// refused over a live stream closes the gate, so BacktestEngine::fill_report +// (the function every generated wrapper calls right after run()) answers empty. +// The snapshot read borrows the rows' own owner while it reads and must restore +// the closed gate on every way out of the read: when it returns, and when it +// throws. Both are observed by reading the batch gate directly AFTER the +// snapshot with no run in between (a later run would reopen the gate and prove +// nothing). The throw is the next allocation made by the read, injected with +// the file's operator new, so the snapshot call fails and hands out nothing. +// Defined fields only: the trade count, the arrays and the bar counters. +void test_snapshot_restores_batch_gate() { + std::vector bars; + for (int index = 0; index < 60; ++index) { + const double price = 100 + index % 7; + bars.push_back({price, price + 2, price - 1, price + 1, 5, + 1577836800000LL + static_cast(index) * 60000}); + } + for (int mode = 0; mode < 2; ++mode) { + pf_strategy_t strategy = strategy_create(nullptr); + assert(strategy); + assert(strategy_stream_begin(strategy, bars.data(), 40, "1", "1") == 0); + for (int index = 40; index < 50; ++index) + assert(strategy_stream_push_bar(strategy, &bars[index]) == 0); + pf_report_t before{}; + assert(strategy_stream_fill_report(strategy, &before) == 0); + assert(before.script_bars_processed > 0); + pf_report_t batch{}; + run_backtest(strategy, bars.data(), 60, &batch); + assert(!std::string(strategy_get_last_error(strategy)).empty()); + assert(batch.total_trades == 0 && !batch.trades && batch.script_bars_processed == 0); + report_free(&batch); + auto* engine = static_cast(strategy); + auto batch_reader_is_empty = [&] { + pf_report_t gated{}; + gated.total_trades = 99; + engine->fill_report(reinterpret_cast(&gated)); + const bool empty = gated.total_trades == 0 && gated.trades_len == 0 && !gated.trades + && gated.input_bars_processed == 0 && gated.script_bars_processed == 0 + && gated.equity_curve_len == 0 && !gated.equity_curve; + report_free(&gated); + return empty; + }; + assert(batch_reader_is_empty()); // closed by the refused call + if (mode == 0) { + pf_report_t snapshot{}; + assert(strategy_stream_fill_report(strategy, &snapshot) == 0); + assert(snapshot.script_bars_processed == before.script_bars_processed); + report_free(&snapshot); + } else { + allocation_failure = 1; + pf_report_t broken{}; + const int broken_rc = strategy_stream_fill_report(strategy, &broken); + const int unconsumed = allocation_failure; + allocation_failure = 0; + assert(unconsumed == 0); // the injected failure fired inside the snapshot read + assert(broken_rc != 0); + report_free(&broken); + } + assert(batch_reader_is_empty()); // still closed: nothing ran in between + pf_report_t again{}; + assert(strategy_stream_fill_report(strategy, &again) == 0); + assert(again.script_bars_processed == before.script_bars_processed); + assert(again.total_trades == before.total_trades); + report_free(&again); + report_free(&before); + strategy_free(strategy); + } + std::cout << "stream snapshot, normal and throwing: batch gate stays closed with no run in between PASS\n"; +} + std::string receipt_field(const std::string& document, const std::string& name, const std::string& field) { const auto row = document.find("{\"name\":\"" + name + "\""); @@ -507,5 +577,6 @@ int main(int argc, char** argv) { strategy_free(strategy); test_stream_snapshot_is_current(); test_refused_batch_over_live_stream(); + test_snapshot_restores_batch_gate(); std::cout << "checked settings: validation, receipt and exception containment PASS\n"; } diff --git a/tests/test_native_c_api.c b/tests/test_native_c_api.c index 39d4825f0..a5b2a17d6 100644 --- a/tests/test_native_c_api.c +++ b/tests/test_native_c_api.c @@ -10224,6 +10224,94 @@ static void check_started_then_failed_report(void) { strategy_native_host_free(state.host); } +/* A genuine nested call. The twin host drives a second strategy_native_run_v1 + * on its OWN handle from inside the calculation callback, five calculations + * after the flatten (the round trip is closed by then), over an output that + * holds indeterminate bytes. The engine refuses a begin while the host runs (a + * contract failure), so the nested call began nothing: it answers E_RUN_FAILED + * with the empty report, overwritten and never freed. The outer run did begin. + * It ends Failed (the nested refusal latched the lifecycle; the lifecycle is + * read here, never assumed Running) and still publishes its OWN partial rows: + * the closed trade stays. Two separate effects of the one nested exit are + * observed: the nested caller's report is empty, and the outer call keeps its + * right to publish. Defined fields only. */ +typedef struct nested_report_state { + twin_state twin; /* the twin callback's own state, first member */ + const pf_bar_t* bars; + int n; + int fired; /* the nested call was made */ + int nested_rc; + pf_report_t nested; /* the nested call's output */ +} nested_report_state; + +static int nested_run_after_trade_on_bar(void* user, const pf_bar_t* bar, + const pf_native_decision_v1* at) { + nested_report_state* state = (nested_report_state*)user; + const int rc = twin_on_bar(&state->twin, bar, at); + if (rc != 0) return rc; + if (state->twin.calculations == PF_TWIN_EXIT_BAR + 5 && !state->fired) { + state->fired = 1; + memset(&state->nested, 0x5a, sizeof(state->nested)); + state->nested_rc = strategy_native_run_v1(state->twin.host, state->bars, state->n, + &state->nested); + } + return 0; +} + +static void check_nested_run_report_ownership(void) { + pf_native_run_spec_v1 spec = twin_spec(); + pf_native_callbacks_v1 table; + nested_report_state state; + pf_report_t report; + px_state after; + const pf_bar_t* bars; + int n = 0; + + memset(&state, 0, sizeof(state)); + memset(&report, 0, sizeof(report)); + table = blank_callbacks(&state); + table.on_bar = nested_run_after_trade_on_bar; + state.twin.host = strategy_native_host_create_v1(&table); + CHECK(state.twin.host != NULL, "nested-report host create failed"); + if (!state.twin.host) return; + CHECK_EQ_INT(strategy_configure_native_v1(state.twin.host, &spec), 0, + "nested-report configure"); + bars = pf_twin_bars(&n); + CHECK(n > PF_TWIN_EXIT_BAR + 5, "the twin bars end before the nested call"); + state.bars = bars; + state.n = n; + CHECK_EQ_INT(strategy_native_run_v1(state.twin.host, bars, n, &report), PF_NATIVE_E_RUN_FAILED, + "a run whose callback made a nested run did not fail"); + CHECK_EQ_INT(state.fired, 1, "the nested run was never made"); + CHECK_EQ_INT(state.twin.command_error, 0, "a command returned an error before the nested run"); + + /* The nested call: refused without beginning, the empty report. */ + CHECK_EQ_INT(state.nested_rc, PF_NATIVE_E_RUN_FAILED, "the nested run was not refused"); + CHECK_EQ_INT(state.nested.total_trades, 0, "the nested call kept a trade count"); + CHECK_EQ_INT(state.nested.trades_len, 0, "the nested call kept trade rows"); + CHECK(state.nested.trades == NULL, "the nested call handed out a trade array"); + CHECK_EQ_INT(state.nested.input_bars_processed, 0, + "the nested call kept the outer run's input counter"); + CHECK_EQ_INT(state.nested.script_bars_processed, 0, + "the nested call kept the outer run's bar counters"); + CHECK(state.nested.equity_curve == NULL, "the nested call handed out an equity curve"); + strategy_native_report_free_v1(&state.nested); + + /* The outer call: it began, so its partial rows are its own. */ + px_read_state(&after, state.twin.host); + CHECK_EQ_INT(after.rc, PF_NATIVE_OK, "outer state read"); + CHECK_EQ_INT(after.lifecycle, PF_NATIVE_LIFECYCLE_FAILED, "the outer lifecycle is not Failed"); + CHECK_EQ_INT(after.failure_code, PF_NATIVE_FAILURE_CONTRACT, + "the nested refusal did not latch a contract failure"); + CHECK_EQ_INT(report.total_trades, 1, "the outer report lost the closed trade"); + CHECK_EQ_INT(report.trades_len, 1, "the outer report lost the trade row"); + CHECK(report.trades != NULL, "the outer report has no trade array"); + CHECK(report.script_bars_processed > PF_TWIN_EXIT_BAR, + "the outer report lost the bar counters"); + strategy_native_report_free_v1(&report); + strategy_native_host_free(state.twin.host); +} + int pf_native_c_api_checks(void) { failures = 0; check_csurface_create_and_configure_refusals(); @@ -10233,6 +10321,7 @@ int pf_native_c_api_checks(void) { check_lifecycle_round_trips(); check_callback_failure_latch(); check_started_then_failed_report(); + check_nested_run_report_ownership(); check_event_polling(); check_event_retention(); check_unrepresentable_quantity(); From 485539d741ea9ed85d71d3319f8952290ad7c6e2 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 14:26:27 +0800 Subject: [PATCH 3/4] Move the stream snapshot bridge into the engine epoch the version guard checks The measured final run (green1, guards/05-native-versions.log) failed the declared source guard scripts/check_native_cpp_versions.py on 1c89e87a5 while the same guard passed on the newmain base 70ddaa3a7: ValueError: pineforge out-of-line method must belong to engine_script_run_v19 Cause: native_stream_snapshot_report was defined after the closing brace of `inline namespace engine_script_run_v19` in src/native_execution_consumer.cpp. The guard compares every qualified `Name::member(` match in `namespace pineforge` with the matches inside the epoch, so the bridge's `NativeExecutionConsumer::fill_snapshot_report(` call counted as outside the epoch (guards-mid/diag.mid.txt: {'NativeExecutionConsumer::fill_snapshot_report': 1}); the next comparison, namespace_functions, would have named the bridge itself. Fix, two line-neutral hunks and nothing else: - src/native_execution_consumer.cpp: the six-line bridge (two comment lines, the three-line definition, one blank line) moves above the epoch's closing brace. The definition and the fill_snapshot_report, rows_are_current and AttemptScope bodies are byte-identical; one comment line gains ", in the engine epoch". The pinned native_run_spec_v3 digest block stays where it was. - src/c_abi.cpp: the forward declaration beside the two native_c_host hooks is reopened in `inline namespace engine_script_run_v19`, on its own single line (the shape market_driver.hpp uses for its native_run_spec_v3 forward declaration), so it names the same entity as the definition. The call pineforge::native_stream_snapshot_report( in strategy_stream_fill_report is unchanged: qualified lookup finds the member of the inline namespace. The mangled name gains the epoch component, and the two files above are the only mentions of the bridge in the tree. No PF_API export, header, layout, hash, checker, allowlist or version changes. Both files keep their line counts (c_abi.cpp 1173, native_execution_consumer.cpp 11557) and every line outside the touched hunks keeps its number, so no file:line anchor and none of the content pins of the published pages move. All 21 pin occurrences were re-hashed by text on this tree and match (the six c_abi.cpp windows start at line 308); 137 anchors into these two files were scanned and none touches a changed line. No documentation changes. Not compiled or run here (local-only lane). A read-only text replay of the guard's ownership comparison (stdlib only, the guard itself not executed) reproduces the measured difference exactly on 1c89e87a5; on this tree TYPE_DEF, ALIAS_DEF, METHOD_DEF and namespace_functions agree for the consumer .cpp and .hpp. The build, ctest, the declared source guards and the doc gates still have to be re-run on a spot box. Co-Authored-By: Claude Sonnet 5.5 --- src/c_abi.cpp | 2 +- src/native_execution_consumer.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/c_abi.cpp b/src/c_abi.cpp index 6d2234981..0e3872339 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -57,7 +57,7 @@ namespace pineforge { void clear_native_c_host_refusal_error(BacktestEngine* engine) noexcept; bool native_c_host_in_postrun_hook_frame(BacktestEngine* engine) noexcept; -void native_stream_snapshot_report(BacktestEngine* engine, ReportC* out); +inline namespace engine_script_run_v19 { void native_stream_snapshot_report(BacktestEngine* engine, ReportC* out); } } namespace { diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index b1f0411cd..fce22a805 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -11530,14 +11530,14 @@ void NativeExecutionConsumer::fill_snapshot_report(const BacktestEngine& engine, consumer->batch_current_ = outcome; } -} // inline namespace engine_script_run_v19 - // The C stream seam's snapshot reader, declared in c_abi.cpp beside the native -// C host's hooks: strategy_stream_fill_report reads through it. +// C host's hooks, in the engine epoch: strategy_stream_fill_report reads through it. void native_stream_snapshot_report(BacktestEngine* engine, ReportC* out) { NativeExecutionConsumer::fill_snapshot_report(*engine, out); } +} // inline namespace engine_script_run_v19 + // The portable run-spec digest lives here, in the one translation unit that // owns `hash_spec` (see its definition above, beside the continuation fold), // so the digest cannot drift from the fields the continuation identity folds From 891da591e2f4604454c201a6dae42bba51d2839e Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Fri, 9 Oct 2026 14:41:10 +0800 Subject: [PATCH 4/4] Correct attempt-report citations and qualify refusal documentation Apply the retained docdelta1 patch after the private bridge epoch repair. This corrects 1c89e87a5 and the earlier 44f7d0f8e citation mapping. Replace the broad claim "A calendar or timezone refusal comes after the consumed run-number high-water moved" with its begin_ready scope. Replace "A begin while the handle is Running" with the actual batch/reentrant path and the repeated-stream exception. Four citations now point at the source they describe. Runtime behavior is unchanged. The author timed out; the patch and sidecar predate the bound, while its detailed report was written after it. Final independent review and exact-head runtime checks remain required. --- docs/design/native-feature-parity.md | 2 +- docs/pages/native-engine.md | 17 +++++++++++------ docs/pages/pine-to-native.md | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 3efc3d6ac..76be5806a 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -1676,7 +1676,7 @@ NativeMetricsView native_metrics() const; // S: net_pro - `KernelRecordedAtHostMarks` (R1, KERNEL-EDGE): the same recording, at the points a C++ host names with `NativeStrategyHost::mark_native_report_point` instead of once per calculation; the consumer never records on its own initiative under it, so the continuation identity stays where `HostRecorded` leaves it. The Pine adapter's existing `mark_script_report_point` route has the same kernel producer. It is what the adapter declares (`KernelRecordedAtHostMarks` pine_adapter.cpp:2790), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5236-5278), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path, `seal_script` native_execution_consumer.cpp:8492-8515), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 2d884c6fb..8940a9580 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -644,7 +644,7 @@ 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:7434-7436`). **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:7404`), the +`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 @@ -4013,14 +4013,19 @@ 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 refusal comes after the consumed run-number -high-water moved, and it fails the lifecycle. A begin while the handle is -`Running` (a live stream, or a call made from a callback) is a contract +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 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. +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 diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index b3a07cbb0..6d376e26d 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,7 +126,7 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:639 | `currency` pineforge.h:499 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:636, `tickerid` native_run_spec.hpp:637, `type` native_run_spec.hpp:638, `basecurrency` native_run_spec.hpp:640, `description` native_run_spec.hpp:641 and `volumetype` native_run_spec.hpp:642 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:502 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:658 | `fee_value` pineforge.h:502 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:4131-4132. TradingView's round-then-slip order is adapter-only. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:652 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:6578-6582. TradingView's round-then-slip order is adapter-only. | | `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:8000-8001, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:664 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:3277). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 13, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2147 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | @@ -463,7 +463,7 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1791 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2506 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | | the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:3039 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5550. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2509 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5948-5952). | | raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2504 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2545 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1485 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. |