diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fc877960..716e7651b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ set(PINEFORGE_KERNEL_SOURCES src/native_calendar.cpp src/native_run_spec.cpp src/native_fx_curve.cpp + src/native_bar_views.cpp src/selected_report_view.cpp src/selected_window_plan.cpp ) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 7110412c6..bca57350b 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -37,7 +37,7 @@ Three layers, two of them meant to be optional: `timeframe`, `timezone`, `str_utils`, `c_abi`, `native_c_host`, `reservation_expansion` (`CMakeLists.txt:126`) and `pending_order_mirror` (`CMakeLists.txt:132`) units — plus the headers directly under `include/pineforge/`. That list is what `add_library` - (`CMakeLists.txt:158`) compiles into `pineforge_kernel` / `PineForge::kernel`, and it and + (`CMakeLists.txt:159`) compiles into `pineforge_kernel` / `PineForge::kernel`, and it and `PINEFORGE_SOURCE_LAYER_SOURCES` are disjoint: no file is in both, and none is outside both. **Two stems exist twice; the kernel copy is the generic half** — `ReservationExpansion` (`src/reservation_expansion.cpp:10`) and @@ -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:7955-7957`) — + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:8023-8025`) — `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:5769`). + (`TermsUnresolved` `native_execution_consumer.cpp:5837`). - **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:5742`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:5810`) | | 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 | @@ -211,11 +211,11 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:823`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:848`), whose - `validate_precommit` (`pine_adapter.cpp:16371`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:16575`) to own the opening margin decision; `on_native_tick` - (`pine_strategy_host.cpp:550`) / `on_native_applied` (`pine_strategy_host.cpp:622`) for + `resolve_execution_terms` (`pine_strategy_host.cpp:828`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:853`), whose + `validate_precommit` (`pine_adapter.cpp:16373`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:16577`) to own the opening margin decision; `on_native_tick` + (`pine_strategy_host.cpp:555`) / `on_native_applied` (`pine_strategy_host.cpp:627`) for `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's MFE/MAE, and `ab9714be`'s host model (RULING A48) is gone. R5 lane H-MEASURE had measured the @@ -237,14 +237,14 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:44, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:11, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:51) and historical birth reach (`order_birth.cpp` `src/compat/pine/order_birth.cpp:5-14`). - **Trail and tick conventions.** The half-tick arm threshold measured against tick-quantized - extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12500-12505`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:12502-12507`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:384) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14844`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:14846`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:558-564`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no closing row - (`on_native_applied` `pine_strategy_host.cpp:622`), inside a live book as well as at a flat + (`on_native_applied` `pine_strategy_host.cpp:627`), inside a live book as well as at a flat reset — broader than `ab9714be`, which reset only a whole book at or under that size. Measured by R5 lane H-MEASURE (`tests/test_pine_dust_sweep_paired.cpp`): TradingView's decimal quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row @@ -260,8 +260,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17830`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:17899`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:17832`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:17901`) for the ten-significant-digit money, and `resolve_margin_call_units` for the lot-floored 4x restore. That hook answers every call on purpose (R5 lane F7): TradingView floors the restore onto the lot grid before the multiple, so on a gridded tape the kernel's own `ShortfallMultiple 4` books 5 lots where TradingView @@ -272,7 +272,7 @@ kernel and own these quirks, each at its site: margin since F7), while the roll point measures at the remaining path's adverse mark (MG-FX: 0.4116 @ 97 there against TradingView's 0.3996 @ 100). What has no kernel check point at all stays adapter-side: the `process_orders_on_close` chronology exception - (`non_pooc_commissioned_short` `pine_adapter.cpp:24165`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:24167`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:34-47`) and the review fold they feed (`awaits_pair_review` `:68-73`, `fold_admission_history` `:83-140`). @@ -333,14 +333,14 @@ kernel and own these quirks, each at its site: projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — the language-state snapshot/restore around a recalculation, the waypoint-only refill deferral - (`next_source_path_waypoint` `pine_adapter.cpp:10464`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:10466`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:848`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6744`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:6746`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` `pine_strategy_host.hpp:1186`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:2223`) selects from the kernel's session-day facts before each source + (`pine_strategy_host.cpp:2228`) selects from the kernel's session-day facts before each source callback — `session_ismarket_` and the `_regular` pair are those facts as they are, and the plain `session.isfirstbar` / `islastbar` pair widens the regular day by the pre- and post-market bars an extended-hours chart holds (`update_extended_session_day`, Pine policy on TradingView's @@ -370,7 +370,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files ## Current state vs. target (read before relying on "without Pine") - **The kernel is a separately linkable artifact.** `add_library` - (`CMakeLists.txt:158`) builds `pineforge_kernel` / `PineForge::kernel` from + (`CMakeLists.txt:159`) builds `pineforge_kernel` / `PineForge::kernel` from `PINEFORGE_KERNEL_SOURCES` alone, with the same flags `pineforge` uses, in **both** configurations. `libpineforge.a` still carries the adapter beside the kernel by default; `-DPINEFORGE_BUILD_SOURCE_LAYER=OFF` (`CMakeLists.txt:44`) compiles no source-layer @@ -448,7 +448,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `native_selected_strategy.cpp`), both of which include `native_host.hpp`. Each C host includes ``, which includes `native_c_api.h`. Every one links `PineForge::kernel` and checks its own numbers before it prints its summary line. `PINEFORGE_BUILD_EXAMPLES` - (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:387`) and + (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:388`) and registers each as a CTest row that runs through `examples/native/run_example.cmake`, which fails on a nonzero exit, a signal **or** a missing summary line — so a row asserts both, which a bare `PASS_REGULAR_EXPRESSION` cannot. `scripts/test_example_runner.py` proves @@ -600,17 +600,17 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (the private fixture set `intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of the private test `test_adapter_margin_schedule_differential`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `pm2-m7-lim-*` of the private fixture set `margin_entry_bar` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the samples of a retained lower bar -- `NativeExecutionConsumer::deliver_intrabar_script` (`src/native_execution_consumer.cpp`): a bar the direct four-sample walk declines (a leg of zero length, an open at its own low) is walked through its own four turning points, the repeated one included, where the general `ENDPOINTS` sampler filled the missing point uniformly; a bar that traded nothing at one price (volume 0, open = high = low = close) is one discrete point, walked once | every host with a `lower_tf` path; the Pine adapter's magnified path marks a chart bar's own open and close this way (R5 lane MAG-INTRABAR, `source::tradingview_magnifier_bars`, `src/source/magnifier_intrabars.cpp`) | **a mechanism correction** (R5 lane MAG-INTRABAR). Rule 2 (a), mechanism: a retained bar supplies its own four ticks, and every sample is a check point of the margin model since R5 lane PAR-MARGIN, so a sample between them measured -- and a request matched at -- a price the bar did not print at that point of its path; a bar that traded nothing is one quote with no path, and its four coincident samples were three path points, three phases and three margin checks that are not there. Nothing in it knows a venue: the rule reads the bar's own prices and volume. Rule 2 (b), knob: none. TradingView's magnified broker walks an intrabar as its four ticks whatever their values (tape `pm2-i3-pooc-mag-0402-2000` of the private fixture set `intrabar_margin`: its first call is booked at an intrabar that opens at its low). What moved: an open-at-its-low bar's second mark is its low, 60, where the sampler put 77.5, and the call armed at that open is booked there; a zero-volume one-price bar gives one mark where it gave four (`tests/test_native_intrabar_turning_points.cpp`, kernel-only), and the sampler test's lower-path expectation moved with it (`tests/test_magnifier_ordered_sampler.cpp`: a sub-bar with a zero-length leg walks its corners). No witness value moved; the corpus runs no lower path (312/312, moved 0). | -| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2360` and `:2490`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | -| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1484`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | -| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1744`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:2365` and `:2495`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:427`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1489`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | +| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1749`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | | the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | | `source_stream_entry_comment` (`include/pineforge/engine.hpp`) | nobody. The kernel's default (`src/engine_consumer.cpp`) and the Pine host's override (`src/source/pine_strategy_host.cpp`) both do nothing, so `stream_observe_entry` keeps the lot's own comment (`src/engine_stream.cpp`) | **dead, kept for the script ABI**. It is a virtual of `BacktestEngine`, which every generated strategy derives from, so deleting it moves every generated script's vtable: an `engine_script_run` epoch. It goes at the next one (`docs/native-refactor-progress.md`, "Scheduled for the next script-ABI epoch") | | the magnifier report facts -- `bar_magnifier_enabled_`, `diag_magnifier_sub_bars_processed_`, `diag_magnifier_sample_ticks_processed_` (`include/pineforge/engine.hpp`) | the kernel, from the run spec's intrabar path and its driver statistics (`src/native_execution_consumer.cpp`). The Pine host writes them again at every bar callback, from its scheduler's magnifier switch and the context's driver statistics (`on_native_bar_open`, `on_native_bar`) | **retained, report-only**: the report's `bar_magnifier_enabled` and magnifier totals (`src/engine_report.cpp`), outside the hashed region. The kernel publishes them for a bare host itself (the C-surface row "kernel-owned report facts" above), so the Pine host's second write is an adapter-thinness question, not a boundary one | | the timeframe report facts -- `diag_script_tf_ratio_`, `diag_needs_aggregation_` (`include/pineforge/engine.hpp`) | the Pine host alone. Its scheduler's run begin stores the ratio of the run spec's script and input timeframes and whether the run aggregates (`diag_script_tf_ratio_` `pine_scheduler_native.cpp:185`, in `PineScheduler::run_begin`), as the retired scheduler's run did before #254. A bare host's report reads 0 for both: the kernel does not write them | **retained, report-only** (R5 lane FIX-E1E2): the report's `script_tf_ratio` and `needs_aggregation` (`src/engine_report.cpp`), outside the hashed region. Execution never reads them | -| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:721`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | -| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2790`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | -| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:660-686`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2427`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1283`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:726`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | +| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:2795`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | +| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:665-691`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:2432`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::entry_time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket`, and on a global exit's re-cut rows `Trade::qty`, `Trade::pnl`, `Trade::pnl_pct`, `Trade::commission`, `Trade::max_runup`, `Trade::max_drawdown` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2), the entry time of a row a later execution of the same pass booked from the lot included (R5 lane FIX-E1E2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion of its own: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. The one exception is the re-cut of a global `strategy.exit` fill. When one fill of an exit that names no entry closes the lots of two or more entry ids and leaves the book flat, the host re-cuts that fill's rows into TradingView's children (`order_global_exit_children`, the private fixture set `global_exit_children`). Under `close_entries_rule = "ANY"` it permutes the rows. Otherwise each child is a transaction of its lot's quantity settled oldest lot first, and each slice is a row carrying its share of quantity, P&L, excursions and ten-significant-digit fees; the kernel's book and fills are untouched. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1288`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | | the closed-row counters -- `win_trades_count_`, `loss_trades_count_`, `eventrades_count_`, `gross_profit_sum_`, `gross_loss_sum_` (`include/pineforge/engine.hpp`) | the kernel, once per row it books (`engine_orders.cpp`); the Pine host re-counts them over the children's rows when it re-cuts a global exit's fill into more rows than the kernel booked (`order_global_exit_children`), so `strategy.wintrades`, `strategy.losstrades` and `strategy.eventrades` count the rows the report shows | **retained: the counts follow the report's rows**. Hashed (`src/engine_state_hash.cpp`) and folded as the host leaves them; the re-cut runs inside the applied notification, before the next hash read. `net_profit_sum_` is not re-counted: the equity stays the kernel's | | the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host: `scheduler_record_range_end` produces report-only terminal rows and preserves ordinary stored marks in REALTIME; `PineStrategyHost::present_report` shapes only the caller-owned report copy through the engine's generic default-no-op presentation seam, before metrics are derived. Historical batch keeps its existing terminal re-mark and extreme fold. Complete-prefix stream reports equal batch from the first realtime script bar; an open position at zero realtime bars retains the warmup mark, and mid-bar reads are host presentation, not a pinned batch boundary. The stream hash contract changes: hashed extremes no longer fold hypothetical terminal marks in REALTIME, so stream and batch terminal hashes need not match; restart/replay binds the exact library digest | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | | `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:159`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | @@ -710,13 +710,13 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:384`), `source_level_on_price_grid` (`:367`), `nearest_tick` (`:274`) / `source_bar_fill_tick` (`:295`) / `directional_tick` (`:327`), behind the terms seam | R5-3 and design risk E7 ruled `None` for the adapter before the lane ran; lanes R7 and N13 measured the alternative anyway (raw levels submitted, `QuantizeFillsAndTriggers` with `HalfUp` declared). L8b closed the first blocker (no run aborts). The second has no remedy on either side: TradingView quantizes per order kind (stop and limit legs and a trail's activation on the quantized bar; the trail stop, the running best, stop-limit entries and the `calc_on_order_fills` cursors raw), the grid is one rule for the run, and the old N13 trial moved 30 pinned checks in 4 units; this is historical evidence, not a current-head result; a per-kind mask would spell that inconsistency into the kernel. The corpus cannot arbitrate (every probe runs a 0.01 tick on an on-grid feed; 5 of 312 differ in an engine-only column). Permanent witness: `tests/test_adapter_grid_relower.cpp`, whose section 5 pins the trail stop the grid fires a bar early. Design row PG and §3.6 | `examples/native/native_price_grid_strategy.cpp`, `examples/native/native_price_grid_c.c`, the private test `test_native_price_grid` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17481`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20856`), `chart_day_key` (`pine_adapter.cpp:17378`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:345`) and the kernel's `configure` (`pine_strategy_host.cpp:354`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | -| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16490-16491`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17483`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20858`), `chart_day_key` (`pine_adapter.cpp:17380`), `compat::pine::IntradayCap` with `IntradayOrderBudget` (337 lines) | Structural first: Pine's risk calls are per-bar statements, so a limit reaches the adapter on script bar 0, after `project()` (`pine_strategy_host.cpp:350`) and the kernel's `configure` (`pine_strategy_host.cpp:359`) have fixed and digested the spec. In substance (lane N12, `tests/test_adapter_risk_relower.cpp`, 62 checks over nine paired scenarios): the drawdown latch samples at the close only and still admits a reversal; the loss-day streak counts trades, not days; the intraday loss closes at the path's adverse extreme, refuses every placement and withdraws the book; the fill cap charges slots, transfers quota and closes at the bar's better extreme on the chart timezone's day. With the kernel seeded on the corpus, 3 of 4 cap probes diverge (3840 of 3916, 312 of 604, 2370 of 2384 rows; re-derived by `scripts/check_seeded_risk_experiment.sh` since R5 lane H-MEASURE). `strategy.risk.allow_entry_in` is the one rule that is the kernel's already (`allowed_open_directions`). Design §3.6 | `examples/native/native_risk_limits_strategy.cpp`, `examples/native/native_trail_risk_strategy.cpp`, `tests/test_native_risk_limits.cpp`, `tests/test_native_c_api.c` | +| `max_abs_units` | **adapter-policy** | `strategy.risk.max_position_size` as a gate on the LIVE book before the fill (`max_position_size` `pine_adapter.cpp:16492-16493`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | | `max_open_lots` | **adapter-policy** | Pine pyramiding is a per-cycle entry count in the adapter's command policy; a resting source entry must not consume a physical-lot cap before it fills, so `project()` leaves the cap unset (`max_open_lots` `pine_adapter.cpp:2646-2648`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (the private test `test_pyramiding_count_differential`, 15 scenarios on three `lab tv` tapes): TradingView checks an entry once, at its first eligible point, against the trades then open, and does not check a resting entry again at its fill; the kernel's cap books 14 of the 15 as TradingView does, the adapter's per-cycle count 13 (it keeps a slot a close or another entry's exit drained; since lane W8A-SIGSTATE-1's R-B a resting LIMIT entry no longer counts, and one the count admitted fills over the cap, and 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, as on TradingView's tapes). A recorded divergence: the retention stands pending a re-lowering ruling. R5 lane H-THIN tried the lowering and kept the count: on the kernel's cap four corpus probes moved away from TradingView, for want of the first-eligible-point rule (H-MEASURE's P4b); it fixed the reservation divergence measured beside it (P10), so a default `strategy.exit(from_entry)` under FIFO now reserves its entry's own quantity, the oldest lots first, as TradingView does | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.cpp` | | `initial_margin_fraction` | **adapter-policy** | TradingView's ten-significant-digit money admission against the signal-time tuple, answered as `AdmitWithHostMargin`; the `margin` model the adapter does declare is maintenance-only (`NativeMarginModel` `pine_adapter.cpp:2885-2893`, `margin.maintenance_long` `pine_adapter.cpp:2900`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes. Measured both ways by R5 lane H-MEASURE (the private test `test_adapter_margin_schedule_differential`, M11): the kernel's gate declines a 1x long whose entry fee the adapter admits (`InitialMargin`), and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times | `tests/test_native_precommit_view.cpp`, `tests/test_native_margin_model.cpp` | -| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1982): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | +| `report_open_position_at_end` | **adapter-policy** | The Pine host owns TradingView's range-end shape: batch re-marks the stored final point and re-folds extremes, while REALTIME retains ordinary marks and `PineStrategyHost::present_report` re-marks only the caller-owned copy before generic metric derivation; the stream hash excludes that terminal extreme fold (`scheduler_record_range_end` pine_strategy_host.cpp:1987): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2855) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | | `open_bar_view` | **adapter-policy** | `Complete`: TradingView's bar-open scheduling and its `calc_on_order_fills` callback read the whole script bar (`pine_adapter.cpp:2825-2827`) | design rows CT4 and E5: the open-only view is opt-in because the adapter needs the full bar | `examples/native/native_calc_on_fills_strategy.cpp`, `tests/test_native_calc_timing.cpp` | -| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1712`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1703`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1717`): a plain `request.security` site is a kernel subscription | lane R3b over the L6c hook: 21 of the corpus's 23 `request.security` probes run their sites on the kernel, byte-identical. The predicate (`declare_security_sites_to_kernel` `pine_strategy_host.cpp:1708`) declares every site of a run or none. A run keeps all of its sites on the source evaluator when it is a stream's warmup, runs the bar magnifier, cuts its range start to na (KI-55), projects historical lookahead, installs an auxiliary feed, aggregates its chart or has no detected timeframe. It keeps them there too when any one site uses `lookahead_on`, `ticker.heikinashi`, a lower timeframe (requested, emulated, read from the input, or as an array), a publication gate, or a calling-bar close or open latch; when a forex or CFD intraday chart without a native feed asks for `D`; when a site asks for N days, whose bars TradingView counts in the symbol's trading dates from each year's first (`tests/test_security_multi_day_buckets_tapes.cpp`), not on the kernel's calendar-day grid; when the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | | `auxiliary_feed` | **adapter-policy** | the adapter's own auxiliary drive: the chart-slice mapping and the deferred first bucket (`src/source/pine_aux_security.cpp`) | lane N7, retained on three measurements: 0 of 312 corpus probes install an auxiliary feed; TradingView's chart slice leaves pre-range coverage inert where the kernel folds it by time, and evaluates after the bar's matching pass where the kernel delivers before it (`tests/test_native_auxiliary_feed_twin.cpp`, rows B and C). Design §2.iv | `tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp` | | `quantity_tolerance` | **adapter-policy** | TradingView's `1e-10` quantity rules on top of the exact default: a close within `1e-10` of the held position is a `Flatten`, a batched process-orders-on-close (POOC) close ending within `internal::kQtyEpsilon` (`1e-10`) of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`); an ambiguous ordinary deferred FIFO `strategy.close` instead uses `HostSized` to authenticate the exact physical FIFO prefix sum for its `Reduce` at fill time, with that same tolerance (`source_fifo_close_units`, `src/source/pine_adapter.cpp`). Detection checks only the current prefix and prefixes reachable after already-pending same-bar closes, so non-ambiguous request shapes, hashes and readback remain unchanged. Same-bar market transactions, `immediately` closes and the ShortSeed placeholder remain out of scope, and after every fill `PineStrategyHost::on_native_applied` erases lots of at most `kQtyEpsilon` without a closing row (`src/source/pine_strategy_host.cpp`) | lane K-ULP4 (the owner-level ruling of audit X1, part b): opt-in and absent by default, so every adapter run is byte-identical by construction. The tolerance charges a snapped `Reduce` its request and books every dust lot it closes as a row, where the adapter's prefix `Flatten` is charged the lots it holds and its sweep books no row; declaring it would move the adapter's fills, ledgers and hashes, so whether the adapter can move onto it is a later lane's measurement. R5 lane H-THIN measured each of those comparisons (design §3.9b) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | @@ -764,7 +764,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:980`) 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:75-76`) | **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:8681-8693`); 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:1125-1130`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:75-76`) | **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:8749-8761`); 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:1125-1130`) 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:9767-9770` (lookahead) and `clear_if_gapped` `:9754-9757` (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:9835-9838` (lookahead) and `clear_if_gapped` `:9822-9825` (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 | — | the private test `test_native_wm_buckets` | @@ -837,12 +837,12 @@ 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:8161`, 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 (the private fixture set `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 | the private test `test_native_session_day_facts` (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, the private fixture set `session_windows`), the C scenario of `tests/test_native_c_api.c`, the private test `test_session_day_facts_adapter` (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:8918-8934` | 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:8893-8914` | 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:7440` | 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:7163` | 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:1646`, `:1687`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2389`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (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 | +| `present_session_day` `src/native_execution_consumer.cpp:8229`, 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 (the private fixture set `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 | the private test `test_native_session_day_facts` (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, the private fixture set `session_windows`), the C scenario of `tests/test_native_c_api.c`, the private test `test_session_day_facts_adapter` (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:8986-9002` | 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:8961-8982` | 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:7508` | 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:7231` | 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:1651`, `:1692`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:2394`). A bare host never calls the builder, so its calendar stays nominal | the private test `test_o_close_pct_day_anchor` (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) | | the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:799`, `price_grid_decimals` `include/pineforge/engine.hpp:833`, `level_on_price_grid` `include/pineforge/engine.hpp:843`, `apply_qty_step` `include/pineforge/engine.hpp:932`, `apply_exit_qty_step` `include/pineforge/engine.hpp:952` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | @@ -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:2412`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:3029`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2362`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2465`) | (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, the private fixture set `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:2462`, called at the end of `begin_ready`, once every feed is installed (`prepare_day_labels` `src/native_execution_consumer.cpp:3097`) and read where a script interval resolves (`script_interval_resolved` `src/native_execution_consumer.cpp:2412`); a lookup by a moved label is its bar's own (`day_label_origin` `src/native_execution_consumer.cpp:2515`) | (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, the private fixture set `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/cr150-contracts.md b/docs/design/cr150-contracts.md index 146f30d94..f103c497e 100644 --- a/docs/design/cr150-contracts.md +++ b/docs/design/cr150-contracts.md @@ -1,10 +1,11 @@ # CR-150 P00 declaration contracts -Authority: `core-cr150-design/work/DESIGN.md`, final revision 6 (2026-10-18). -SHA-256 recorded by the P00-7 gating read: +DESIGN coordinate basis: `core-cr150-design/work/DESIGN.md`, revision 6 text +identified by the P00-7 gating read's SHA-256: `68bda6e37931c096522a348603623c7f3cba2b1371a9c0a442edbf53d25ab0ad`. -All DESIGN line citations below use that final text. Dispatch: the external -design lane's `PARALLEL-PLAN.md`, lines 27–30 and 63. C++ declarations spell +DESIGN line citations below use that pinned text; the amendments below take +precedence. Dispatch is the external design lane's `PARALLEL-PLAN.md`, +lines 27–30 and 63. C++ declarations spell the operations described by DESIGN; OPEN items below preserve its unspecified policies. @@ -13,10 +14,21 @@ CORE-TOP's rev 6 amendment (10-18 16:04) in the external design lane's References to amendment S1 or T1 below name that ruling, which binds L6b-1 and L8a respectively. -These are declarations only. No existing file, execution path, feature probe, -version or caller-visible layout is changed. Definitions and lab proof belong -to CORE-TOP and the implementation lanes. Nothing is built, compiled or tested -on the P00 machine. +CORE-TOP's rev 6 amendment recorded at 10-18 19:24 in the same external +`core-cr150-design/work/REVISION.md`, lines 351–358, resolves V1 and V2. It +adopts `core-cr150-l15a/PROPOSAL-V1-V2.md`, SHA-256 +`b17ff9f2dab20d876df2e3148ca1c2aba2b5d78f7ccf8c4f026643cfdb14674e`, +with required changes 1–9 of `core-cr150-read-v1v2/REVIEW.md`. That ruling +also approves eager provider/feed component digests at configure, superseding +the lazy timing in DESIGN 5:1243-1246. The review notes that the later 19:11 +converter amendment shifts live DESIGN coordinates at/after 1173 by +3; +the pinned coordinate basis here remains explicit. + +The freeze adds declarations, value-type defaults and the provider's approved +defaulted virtual destructor. Existing execution paths, feature probes, +versions and caller-visible layouts stay unchanged. Operation definitions +and lab proof belong to CORE-TOP and the implementation lanes. Nothing is +built, compiled or tested on the P00 machine. ## Sink ABI and line writer @@ -91,18 +103,99 @@ private files, as DESIGN permits. No mapped scratch or text-based metrics. ## Bar and close views -| File / declaration | DESIGN section and lines | Implementing lane | +| File / declaration | DESIGN / V1-V2 amendment authority | Implementing lane | |---|---|---| | `include/pineforge/native_bar_views.hpp`: `inline namespace native_bar_views_v1` | 2.2:339-347; 6:1328-1329 | L15a | | `NativeBarView { const Bar* data; std::size_t size; }` | 2.2:348 | L15a | | `NativeInstrumentFeedView { NativeBarView bars; const int64_t* close_ms; }` | 2.2:349 | L15a/L15b | -| `class NativeLowerPathSource;` (forward declaration only; OPEN V2) | 2.2:350-351; 5:1237-1262 | L15a/L16 | +| `NativeInstrumentFeedReadView { NativeBarView bars; const int64_t* close_ms; size_t close_count; }` | 2.2:354-375; proposal V1 | L15a | +| `NativeBarViews` with nullable indexed feed slots, const provider and current segment | 2.2:352-375; 5:1250-1259; changes 1-4 | L15a | +| `native_instrument_feed_view(owned,borrowed) noexcept` (no default argument) | 2.2:355-375; change 3 | L15a | +| `normalize_native_run_spec(spec,views) noexcept` | 2.2:361-363; change 5 also covers projection | L15a | +| `validate_native_instrument_feeds(feeds,views) noexcept` | 2.2:361-363; proposal V1 | L15a/L15b | +| `validate_native_timeframe_subscriptions(...,views) noexcept` | 2.2:361-363; proposal V1 | L15a | +| `native_instrument_feeds_digest(feeds,views) noexcept` | 2.2:364-375; proposal V1 | L15a | +| `native_run_spec_digest(spec,views)` (may throw) | 2.2:364-375; eager-digest amendment / change 8 | L15a | +| `class NativeLowerPathSource` (forward declaration retained and class completed) | 2.2:350-351; 5:1237-1262; proposal V2 as amended | L15a/L16 | +| `NativeLowerPathSource::Visitor = bool (*)(void*,const Bar&) noexcept` | 5:1241-1243; proposal V2 | L16 | +| `virtual ~NativeLowerPathSource() noexcept = default` | Proposal V2: exclusive consumer ownership / base deletion | L15a/L16 | +| `virtual size_t NativeLowerPathSource::count() const = 0` | 5:1241-1247; change 7: lifetime count, O(1) after first call | L16 | +| `virtual bool NativeLowerPathSource::for_each(Visitor,void*) const = 0` | 5:1241-1247; proposal V2 | L16 | +| `virtual NativeBarView NativeLowerPathSource::segment(int64_t,int64_t) = 0` | 5:1250-1257; change 6: repeated/overlapping windows | L16 | +| `virtual const Bar* NativeLowerPathSource::next_after_segment() const noexcept = 0` | 5:1251-1257; proposal V2 | L16 | +| `native_intrabar_path_digest(path,source)` (may throw) | 5:1241-1247; proposal V2 / change 8 | L15a/L16 | + +Staging and the active query are public members of the private +`NativeExecutionConsumer` in `src/native_execution_consumer.hpp` (change 1): + +| Member declaration | Frozen contract | Implementing lane | +|---|---|---| +| `void stage_instrument_feed_views(const NativeInstrumentFeedView* const*,size_t)` | Copy pending descriptors; no lifecycle judgement or active mutation | L15a | +| `void stage_lower_path_source(unique_ptr) noexcept` | Transfer exclusive ownership to pending; null clears pending source | L15a | +| `NativeBarViews bar_views() const noexcept` | Allocation-free active binding only; pending is invisible | L15a | L15a implements staging and the common accessor, with no caller. It also adds the new header/source and namespace check to the native ownership guard (2.2:352-378). Neither the existing v3 run-spec nor native-host declarations change here. The borrowed-feed C export/probe belongs to L15b (6:1318-1329). +### V1/V2 binding and provider rules + +The two original view structs stay byte-for-byte the same. A zero context +feed count selects all owned vectors. Otherwise the count matches metadata +entries by index; a null slot selects its owned feed, while a nonnull slot +always selects borrowed data, including a present empty feed. The read type +preserves the independent owned close count, so validation still detects +length mismatches and the feed digest still folds zero for a missing close. +Borrowed slots require empty owning bar/close vectors, with metadata and +columns retained. Descriptors/table are copied; immutable arrays are borrowed +through every accepted-binding use. Binding adds no pointer registry or +duplicate key/timeframe strings. + +Staging changes pending only. Descriptor allocation throws `std::bad_alloc` +with the previous pending slot preserved; a nonzero count with a null table +throws coded `engine_invariant`. Only an applied configure binds and judges +the pending descriptors/provider against candidate metadata. Refused configure +or unwound preparation discards pending; no Ready restaging. Binding checks +keep the proposal's typed bar/close/metadata refusals and existing validation +order. `bar_views()` exposes active Ready/Running state, never pending, and +returns an empty context when unbound or detached. A detached Ready spec +cannot be admitted, and a provider bound at stream begin is `engine_invariant`. +Run exit, including error/abort, detaches borrowed pointers after retaining +scalar counts/content digests. The provider destructor never reads its input. + +The visitor is synchronous, ordered and nonthrowing; false stops normally. +Whole walks do not overwrite the active segment or separate look-ahead. +Count is fixed for provider life: the first call may walk or use a pre-scan, +later calls return its cached scalar in O(1). Segment bounds are individually +non-decreasing; repeated or overlapping windows return every row in the +half-open interval, including overlap rows. Equal bounds give empty data; +reversed/decreasing bounds are coded `engine_invariant` / `std::logic_error`. +Repeats keep the same storage. A different segment replaces storage only +after its script bar finishes, preparing look-ahead before exposing it. +Look-ahead is the first row at/after the end, even for an empty segment; its +separate buffer stays fixed until a different segment and queries do not +advance. The const context prevents scheduler callbacks selecting segments. + +DESIGN 5 eager-digest amendment: after normalization succeeds and before +configure's first member write, compute immutable feed/provider component +digests. Lower-walk exceptions propagate unchanged and nothing is published; +they are not mapped to validation refusals. Keep components outside combined +cache invalidation and through detachment so the noexcept continuation query +never walks freed input. Standalone context digest overloads may walk/throw. +Folded bytes/order stay the same: feed NaNs canonicalize as before, while the +lower-path digest preserves raw double bits. Null source uses the vector +digest; nonnull source is present even if empty. No failure becomes empty +data, a fallback or a sentinel hash. Unexpected visitor stop/count mismatch +is an invariant error. Runtime segment failures retain the existing intrabar +option rejection; allocation exceptions use the run-entry handler. + +All nine required changes are frozen: (1) consumer staging/query, (2) pending +only and configure binding, (3) explicit accessor argument, (4) const source, +(5) projection validation routing below, (6) overlapping windows, (7) cached +count, (8) propagating eager-digest failures, and (9) RESOLVED V1/V2 text. +The implementations and source guards remain L15a/L15b/L16's work. + ### L15a read inventory This is DESIGN's required source-guard read set (2.2:357-371; 5:1221-1228), @@ -113,18 +206,19 @@ references; L15a must maintain these anchors and mechanically pin every current | Reader to route through the view/source | Source anchors in this tree | DESIGN lines | |---|---|---| -| Kernel instrument-feed pump: current bar, close, next open, count | `NativeExecutionConsumer::pump_instrument_subscription` `src/native_execution_consumer.cpp:9932-9983` | 357-358 | -| Host feed lookup/delivery and column reads | `installed_symbol_feed` `src/source/pine_security_eval.cpp:1059-1068`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1286-1293`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1312-1320` | 359-360 | -| Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:254-276`; `instrument_feed_shapes` `src/native_run_spec.cpp:730`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:995-998`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9468-9471`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9493-9495` | 361-363 | -| Instrument-feed digest and cached/uncached continuation folds | `native_instrument_feeds_digest` `src/native_run_spec.cpp:1124-1160`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:1935-1936`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12730-12735` | 364-365 | -| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-234`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | -| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1536-1544`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | -| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1096`; `feed.bars` `src/source/pine_security_eval.cpp:1420-1422` | 369-370 | -| Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3148-3181` | 371,1221-1222 | -| Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:867-895`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | -| Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8562-8576` | 371,1225 | +| Kernel instrument-feed pump: current bar, close, next open, count | `NativeExecutionConsumer::pump_instrument_subscription` `src/native_execution_consumer.cpp:10000-10055` | 357-358 | +| Host feed lookup/delivery and column reads | `installed_symbol_feed` `src/source/pine_security_eval.cpp:1059-1068`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1291-1301`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1320-1328` | 359-360 | +| Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:259-290`; `instrument_feed_shapes` `src/native_run_spec.cpp:743`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:1019-1022`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9536-9539`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9561-9563` | 361-363 | +| Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `normalize_projected_spec` `src/source/pine_adapter.cpp:2997-3003` | 361-363; V1/V2 change 5 | +| Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:321-323`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2774-2775`; `native_run_spec_digest` `src/native_execution_consumer.cpp:12807-12812` | 364-365 | +| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:230-238`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:292-299` | 366-367 | +| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1545-1553`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1123-1129` | 367-368 | +| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `data.bars` `src/source/pine_security_eval.cpp:1428-1431` | 369-370 | +| Lower-path preflight/validation | `NativeExecutionConsumer::preflight_intrabar_path` `src/native_execution_consumer.cpp:3216-3249` | 371,1221-1222 | +| Lower-path digest (count first) and continuation hash | `native_intrabar_path_digest` `src/native_run_spec.cpp:891-919`; `native_intrabar_path_digest` `src/native_execution_consumer.cpp:288` | 371,1223-1224 | +| Per-script-bar lower-path binary search/walk | `candidate` `src/native_execution_consumer.cpp:8630-8644` | 371,1225 | | Scheduler sub-bar lookup and one-bar look-ahead | `PineScheduler::lower_path_bar_at` `src/source/pine_scheduler_native.cpp:252-264`; `next_open` `src/source/pine_scheduler_native.cpp:296-298` | 371,1226-1227 | -| Lower-path emptiness test | `inactive_sampler_path` `src/source/pine_adapter.cpp:3420-3422` | 371,1228 | +| Lower-path emptiness test | `inactive_sampler_path` `src/source/pine_adapter.cpp:3422-3424` | 371,1228 | Every count/digest must fold the same bytes/order for copied and borrowed feeds, including cached and uncached paths (2.2:372-375). Views replace storage; @@ -138,8 +232,8 @@ metadata, validation and continuation identity remain the existing contract. | S2 | OPEN | Internal writer error transport, oversized-line/unsafe-integer failure details, and what a repeated fill/capture does after a trailer; DESIGN requires repeatable metric reads but gives no writer reset/idempotence contract (3.3:550-551; 3.7:790,881-888). | CORE-TOP / L6a/L6b-1 | | T1 | RESOLVED | Amendment T1: clear() resets size/base to 0; reserve(size_type) is a hint; both at(size_type global) forms throw engine_invariant below base() or at/after size(); empty() const means size() == 0. References and iterators invalidate as a std::vector's. | L8a | | M1 | OPEN | Point-source malformed-region/bounds/short-read error transport and concurrent-reader rules are not specified; the 24-byte binary points, 64 KiB pread window and repeated forward walks ARE fixed (3.3:535-546; 3.8:902-909). | CORE-TOP / L6b-4/L7c | -| V1 | OPEN | Typed staging/accessor signatures, feed binding/lookup and absent-versus-present-empty view representation are not specified. DESIGN names stage_instrument_feed_views / stage_lower_path_source but no prototypes; none are invented (2.2:352-356). | CORE-TOP / L15a | -| V2 | OPEN | NativeLowerPathSource operation return/parameter types, visitor protocol and dispatch/error model are not specified. Only its forward declaration and required behavior are frozen; typed members await a ruling (2.2:350-351; 5:1237-1262). | CORE-TOP / L15a/L16 | +| V1 | RESOLVED | CORE-TOP 19:24 amendment: consumer pending-only staging, active query, indexed nullable slots, explicit accessor and context validation/digest overloads, as amended by required changes 1-5 and 8-9. | L15a/L15b | +| V2 | RESOLVED | CORE-TOP 19:24 amendment: four-operation virtual provider with nonthrowing visitor, const read context, cached count, overlapping/repeated segments, separate look-ahead and exclusive consumer ownership; eager configure component digests propagate failures (changes 1-2,4,6-9). | L15a/L16 | S1's model is the observation query, `strategy_execution_observation_v1` `include/pineforge/execution_observer.h:134-141`. diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 2442b75c8..d01bc41e7 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,123 +47,123 @@ 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:3434; `entry()` pine_adapter.cpp:7157, `order()` pine_adapter.cpp:14610) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6511; `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:10541 → `Flatten{}` pine_adapter.cpp:10575; `Reduce{ExplicitUnits}` pine_adapter.cpp:9414; `ReverseTo` pine_adapter.cpp:7833) | **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:4361 → `host.replace` pine_adapter.cpp:4786) | **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:14504, `cancel_all` pine_adapter.cpp:14577; `live_by_source_key_` pine_adapter.hpp:2521; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4799) | **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. | -| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5005; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9160) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | -| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6577, `cohort_for` pine_adapter.cpp:3010; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9255) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | -| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3443) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:3436; `entry()` pine_adapter.cpp:7159, `order()` pine_adapter.cpp:14612) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:6579; `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:10543 → `Flatten{}` pine_adapter.cpp:10577; `Reduce{ExplicitUnits}` pine_adapter.cpp:9416; `ReverseTo` pine_adapter.cpp:7835) | **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:4363 → `host.replace` pine_adapter.cpp:4788) | **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:14506, `cancel_all` pine_adapter.cpp:14579; `live_by_source_key_` pine_adapter.hpp:2521; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4801) | **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. | +| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:5007; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9162) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances `Allowance` native_order.hpp:662-675 | K | — | O:A16 S:O4 | — | +| OL6 | Ownership: legs bound to a parent / opening(s) / cohort; close the lots of a given entry, FIFO or ANY (`owner_for_close` pine_adapter.cpp:6579, `cohort_for` pine_adapter.cpp:3012; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9257) | **yes** — `Owner` native_order.hpp:464-479, `Authority` native_order.hpp:620-621, `CohortClose` native_order.hpp:617-619, `SelectedExposure` native_order.hpp:625-629, `cohort_open` native_host.hpp:1282, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:3445) | **yes** — `Group` / `GroupEffect` native_order.hpp:466-473, `CancelReason::Group` native_order.hpp:948-953, `ReservationReducedEvent` native_order.hpp:1191-1201 (deferred form `DeferredGroupAdjustmentEvent` native_order.hpp:1211-1221) | K | — | F:A12 O:A6 O:A7 S:O7 | — | | 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:11034; lowering `submit_one` pine_adapter.cpp:11912, `BindCohort` pine_adapter.cpp:12375, `submit_leg` pine_adapter.cpp:11668; 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:1940); 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:11294; `materialize_relative_exits` pine_adapter.cpp:13983, pine_adapter.hpp:2259-2271) | **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:6439. `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-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27061) | **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:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4192, `materialize_snapshot` pine_adapter.cpp:13957), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21615, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21849) | 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. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11036; lowering `submit_one` pine_adapter.cpp:11914, `BindCohort` pine_adapter.cpp:12377, `submit_leg` pine_adapter.cpp:11670; 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:1940); 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:11296; `materialize_relative_exits` pine_adapter.cpp:13985, pine_adapter.hpp:2259-2271) | **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:6507. `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-1484; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:27063) | **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:2515-2534); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1364); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:4194, `materialize_snapshot` pine_adapter.cpp:13959), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1357-1384, `DeferredOpenMarketableSell` pine_adapter.hpp:1845-1868, `CloseCallsiteState` pine_adapter.hpp:1983-1997; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:21617, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21851) | 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:1261; `select_exit_activation` exit_activation.cpp:20-90, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:1078) | 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:2123); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:2200). 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 | ### 1.2 Trailing | 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:6520; `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:6588; `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:15001) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5255) **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:14918) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4754) | **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:11156, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14918, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11153, `trail_one_shot` pine_adapter.cpp:12499 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:15003) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:5323) **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:14920) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4756) | **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:11158, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:14920, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11155, `trail_one_shot` pine_adapter.cpp:12501 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7192, pine_adapter.cpp:7192) | **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:5740, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5641-5675 | 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:3199) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5769 **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:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3070) | **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:3218, `typed_entry_units` pine_adapter.cpp:3236; `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:3191; `percent_commission_live_equity` pine_adapter.cpp:5090) | **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 the private fixture set `cash_fee_sizing`) | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3025, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8466) | **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`. | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:7194, pine_adapter.cpp:7194) | **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:13728-14956) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:5808, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:5709-5743 | 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:3201) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:5837 **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:558 (not the equity: R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-sz-*), money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:658 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:3072) | **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:3220, `typed_entry_units` pine_adapter.cpp:3238; `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:3193; `percent_commission_live_equity` pine_adapter.cpp:5092) | **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 the private fixture set `cash_fee_sizing`) | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:3027, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8468) | **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:505; 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:5210, `compute_exit_reservation` pine_adapter.cpp:5250) | **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:5250, `reserved_exit_qty` pine_adapter.cpp:11563) | **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:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5232) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1746 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8509), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10033), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16212), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16743, pine_adapter.cpp:16796) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21663) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3199), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3218, `typed_entry_units` pine_adapter.cpp:3236), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26648) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | -| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9160; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:5212, `compute_exit_reservation` pine_adapter.cpp:5252) | **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:5252, `reserved_exit_qty` pine_adapter.cpp:11565) | **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:558, `source_money_floor_lot` pine_adapter.cpp:658; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:5234) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1746 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| SZ10a | *(quirk, measured retention)* The money band's affordable price, `sig10(sig10(E) / notional per unit of price)` against the sizing price — at placement (`entry`, `affordable_price` pine_adapter.cpp:8511), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10035), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16214), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16745, pine_adapter.cpp:16798) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21665) | n/a — not a units conversion. Every money-to-units conversion in the adapter is the kernel's `native_sized_units` under the source's floors since R5 lane B-ADAPTER: the default at placement and at the fill (`default_sizing_units` pine_adapter.cpp:3201), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3220, `typed_entry_units` pine_adapter.cpp:3238), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26650) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes and 202 famr tapes (two tests that live in the private fixtures); on a seven-digit tie (E 9998.99999996, 10 000 lots at tick(close) 0.9999) the rule admits while the kernel's conversion buys 9999.99999996 lots and binary64 gives E/Q 0.999899999996 (`tests/test_adapter_fill_qty_probe.cpp`, section 2). | +| SZ11 | *(quirk)* Same-bar `strategy.close` callsite batching, two-call provenance, entry-id ledger (`enqueue_pooc_fifo_close` pine_adapter.cpp:9162; ledger `close_batch_bar_` pine_adapter.hpp:2595; the kernel's comment-only residue was deleted by R5 lanes E6 and E10) | n/a | A | — | S:O8 F:A15 O:A15 | **Disagree:** S "generic → kernel" (its P2 `ReplaceByKey` / `Fifo`). R5-4: adapter. | | SZ12 | *(quirk)* POOC reservation-growth market-entry population predicate (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9-34, called from the adapter, `select_reservation_growth_sources` pine_adapter.cpp:1322) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3621 | 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:3628 | 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:16491) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3633 | 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:7697, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) 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: the private fixture set `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:11587). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17304 and leaves the kernel gate unset pine_adapter.cpp:17323) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3651 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:3497 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 (the private test `test_adapter_margin_schedule_differential`). **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` (the private test `test_explicit_market_admission_tapes`), 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:17557, `margin_pct` pine_adapter.cpp:17059) | **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:17944, `submit_margin_call_units` pine_adapter.cpp:17977, `schedule_margin_call_path` pine_adapter.cpp:20222) | **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:3610 **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:17601) | **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; the private test `test_adapter_margin_schedule_differential` 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:19129; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `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:17601; lane INT28-FIX rule OU, the private fixture set `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:19171, `follow_margin_call` pine_adapter.cpp:19220; R5 lane TAIL-I, the private fixture set `margin_open_print`). | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2859) | **yes** — `allowed_open_directions` native_run_spec.hpp:665 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:3689 | 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:3696 | 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:16493) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:664 → `MaxOpenLots` native_execution_consumer.cpp:3701 | 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:7699, cap left unset `max_open_lots` pine_adapter.cpp:2858-2859) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (the private test `test_pyramiding_count_differential`) 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: the private fixture set `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:11589). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17306 and leaves the kernel gate unset pine_adapter.cpp:17325) | **yes** — `initial_margin_fraction` native_run_spec.hpp:666; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:3719 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:3565 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 (the private test `test_adapter_margin_schedule_differential`). **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` (the private test `test_explicit_market_admission_tapes`), 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:17559, `margin_pct` pine_adapter.cpp:17061) | **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:17946, `submit_margin_call_units` pine_adapter.cpp:17979, `schedule_margin_call_path` pine_adapter.cpp:20224) | **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:3678 **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:17603) | **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; the private test `test_adapter_margin_schedule_differential` 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:19131; R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `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:17603; lane INT28-FIX rule OU, the private fixture set `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:19173, `follow_margin_call` pine_adapter.cpp:19222; R5 lane TAIL-I, the private fixture set `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:5677, `apply_fx_opening_margin_slice` pine_adapter.cpp:5689) | **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) | -| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17481) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | -| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16484) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:49 **Closed:** `NativeRiskLimits::max_consecutive_loss_days`, settled from each day's realized result at the next day's open (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E2 S:M6 | R5-9 | -| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:17514, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20856) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | -| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20899) | **no** **Closed:** `NativeRiskLimits::max_fills_per_day` counts every applied execution of the day as it settles, whatever issued it (`tests/test_native_risk_limits.cpp`). | K+A | L9 | F:D8 O:E4 S:M7 | Count cap K; quota transfer, POOC deferral, TV close price A | -| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:17378) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | -| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17546, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17635, POOC chronology exception `resting_order` pine_adapter.cpp:20242 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18539, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18539) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4544, `record_market_review` pine_adapter.cpp:21155) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:5679, `apply_fx_opening_margin_slice` pine_adapter.cpp:5691) | **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) | +| MG10 | Max drawdown halt (`update_risk_state` pine_adapter.cpp:17483) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1301 **Closed:** `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311, a `NativeLossLimit{value, percent}` measured against the running peak equity at three points of every script bar, blocking openings for the rest of the run (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E1 S:M6 | R5-9 | +| MG11 | Max consecutive losing days (`SourceDayLedger` pine_adapter.hpp:1419-1427; gate `max_cons_loss_days` pine_adapter.cpp:16486) | **no** — observation only `loss_days_limit` include/pineforge/source/market_admission.hpp:49 **Closed:** `NativeRiskLimits::max_consecutive_loss_days`, settled from each day's realized result at the next day's open (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D6 O:E2 S:M6 | R5-9 | +| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:17516, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20858) | **no** — observation only `intraday_loss_limit` include/pineforge/source/market_admission.hpp:51 **Closed:** `NativeRiskLimits::max_intraday_loss` with `NativeRiskAction::FlattenAndBlock`, under which the kernel issues one `Flatten` of its own at the breach point (`RequestOrigin::KernelRisk`) and latches the block before that flatten settles (`tests/test_native_risk_limits.cpp`). | K | L9 | F:D7 O:E3 S:M6 | R5-9 | +| MG13 | Max filled orders per day (`IntradayCap` intraday_cap.hpp:84-263, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:20901) | **no** **Closed:** `NativeRiskLimits::max_fills_per_day` counts every applied execution of the day as it settles, whatever issued it (`tests/test_native_risk_limits.cpp`). | K+A | L9 | F:D8 O:E4 S:M7 | Count cap K; quota transfer, POOC deferral, TV close price A | +| MG14 | Risk-day boundary (`chart_day_key` pine_adapter.cpp:17380) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:416-419, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1329 reads the whole ledger back — block and reason, day, fills counted, loss-day streak, peak equity, day-opening equity (`tests/test_native_risk_limits.cpp`). | K+A | L9 | O:E5 S:M7 | The chart-day shortcut stays A; the kernel keys on session / calendar day | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:17548, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17637, POOC chronology exception `resting_order` pine_adapter.cpp:20244 (the commissioned carried short; the competing-entry and rounded-money exceptions retired by R5 lane W5B-ENG-MARGIN-RESIDUAL, the private fixture set `margin_residual` w5b-pc-*), 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:18541, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18541) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1. **Measured (R5 lane H-MEASURE)** against the kernel's own points on the same bars and orders, the kernel's hooks answering TradingView's money and slice (the private test `test_adapter_margin_schedule_differential`): a gap-open breach executes at the open, sized on the open's money, where the kernel's mark check rests at the path's adverse extreme and `CalculationOnly` executes at the calculation (M8); the pre-open slice parts from the kernel's `AfterApplied` point, and from `ab9714be`, on the one-contract band and the restore's `+1e-6` floor (M10; the frozen units it also parted on are TradingView's book since R5 lane PAR-MARGIN froze a default-percent stop above 100 % as eight `lab tv` tapes, the private tapes `pm-m10-*` of `margin_entry_bar`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (the private fixture set `margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low, is admitted by `on_applied` since R5 lane PAR-MARGIN on the run shapes `ab9714be` checked it on (the pre-open slice's own opening keeps that slice's verdict), and the adapter books the tapes' rows (M7; H-MEASURE had recorded it as a divergence); R5 lane PAR-MARGIN-2 extended it to limit openings, `process_orders_on_close`, `calc_on_order_fills` and adds to a carried book, and made the kernel's post-fill point reach the low a limit was filled on the way down to, or a short's high on its way up (twenty-eight `lab tv` tapes, the private tapes `pm2-*` of `margin_entry_bar`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged long, plain, under `process_orders_on_close` or `calc_on_order_fills`, and on a short outside `process_orders_on_close` (PAR-MARGIN-2), where TradingView books its call at the first lower-timeframe low that crosses -- at its own 2-minute intrabars on a 15-minute chart, which a model reproduces on all 20 magnified tapes (the private fixture set `intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. A margin call books the nearest tick of its print, as TradingView's market fills do (12.105 books 12.11; the private fixture set `half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:34 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:48); `admission_allocation` pine_adapter.cpp:4546, `record_market_review` pine_adapter.cpp:21157) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing | 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:8081 (call site `on_native_recalculate` native_execution_consumer.cpp:8109) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8525 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22250) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7955; `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:7898 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7949 (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:8149 (call site `on_native_recalculate` native_execution_consumer.cpp:8177) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2816) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:8593 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:22252) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:8023; `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:7966 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:8017 (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:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6674, `flush_coof_tail` pine_adapter.cpp:7098, `end_coof_recalc` pine_adapter.cpp:6731) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7802, native_execution_consumer.cpp:7802; commands are legal there `commands_allowed` native_execution_consumer.cpp:1836; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6439, `remaining_path_delivery` native_order.cpp:2082-2090. 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:8037; 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:291. | -| 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:8613 **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:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8184-8450; `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:698 | 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:6947), 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:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:727, `coof_recalculation_due` pine_scheduler_native.cpp:693, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:848; `begin_coof_recalc` pine_adapter.cpp:6676, `flush_coof_tail` pine_adapter.cpp:7100, `end_coof_recalc` pine_adapter.cpp:6733) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:7870, native_execution_consumer.cpp:7870; commands are legal there `commands_allowed` native_execution_consumer.cpp:1843; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:6507, `remaining_path_delivery` native_order.cpp:2082-2090. 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:8105; 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:295. | +| 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:8681 **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:2934) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:8252-8518; `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:698 | 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-311) 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:6949), 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:848, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1961-1991 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | 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:1519, 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:1520 **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:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1871 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1651; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1889 **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:9422 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). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1524, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1534-1833; 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:1525 **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:937) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1878 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1656; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1896 **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:9490 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:832) | **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 (the private fixture set `coof_recalc_security`): the bar's first recalculation before its close callback feeds it (`scheduler_feed_aux_security` pine_scheduler_native.cpp:772). The close callback fed it alone (`scheduler_feed_aux_security` pine_scheduler_native.cpp:634, 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:8918, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8925, 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:8986, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:8993, 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:6511, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7101; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7097) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5766) | 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:5402, clamped by `protect_limit` native_execution_consumer.cpp:7097 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15168) 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:6579, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:7169; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:7165) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5834) | 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:5470, clamped by `protect_limit` native_execution_consumer.cpp:7165 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:15170) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:669) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1683 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:274, `source_bar_fill` pine_adapter.cpp:15168, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7079; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7252; 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_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | 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:274, `source_bar_fill` pine_adapter.cpp:15170, `directional_tick` pine_adapter.cpp:327) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:7147; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:7320; 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_sizing_price_rule.cpp` quantizes the fill basis under `HalfUp`; `tests/test_native_acid_composite.cpp` books the `Directional` fills on the 0.25 ladder; the private test `test_native_price_grid` re-ran the pair under the overlay). | 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_trail_best_seed.cpp` puts an off-ladder trail seed on the ladder; `tests/test_native_acid_composite.cpp` runs a trail's best and its exit level on it; the private test `test_native_price_grid` re-ran the pair under the overlay). | 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:384) 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:9866). **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:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14362), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17770 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14844) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| 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:9934). **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:295 (used `source_trigger_threshold` pine_adapter.cpp:384, `exit_limit_trigger` pine_adapter.cpp:14364), `source_bar_fill_tick` pine_adapter.cpp:295, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:313, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:17772 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14846) | 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:3280, 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 the tape of the private fixture set `e19_allin_trim`. | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:3348, 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 the tape of the private fixture set `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: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:1982) | **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:1444) 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. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1987) | **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:1449) 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:1383, enabled `stream_observe_actions_` native_execution_consumer.cpp:10589, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12152; 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:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2790 **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. | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1383, enabled `stream_observe_actions_` native_execution_consumer.cpp:10666, C ABI `strategy_stream_order_actions_len` c_abi.cpp:737-754 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:12229; 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:593; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:2795 **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 | — | ### 1.9 Other runtime surface @@ -176,8 +176,8 @@ 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:980, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:1051, probe `strategy_execution_contract` c_abi.cpp:971, stream `strategy_stream_begin` c_abi.cpp:698 **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 80 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:323-358, `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 (the private fixture set `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:8161), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2223 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:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). 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:296) | 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 | +| 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:323-358, `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 (the private fixture set `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:8229), so a bare host reproduces the tapes flag for flag (the private test `test_native_session_day_facts`, kernel-only). The adapter computes no regular-day rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:2228 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:1186; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (the private test `test_session_day_facts_adapter`). 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:300) | 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 | --- @@ -185,7 +185,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.i Kernel-only build target (L1) -- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added in an earlier commit, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:167-170), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:158), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:471-476). +- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added in an earlier commit, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:168-171), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:159), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:472-477). - **Link feasibility:** no kernel TU and no public kernel header includes `source/` or `compat/pine/` (F, O). The only code coupling is the forward-declared opaque `source::StrategyOverrides*` (§2.ii a), which needs no definition. - **Risk:** the archive member order of `libpineforge.a` changes. Harmless for static linking, but check `PINEFORGE_REQUIRE_ABI_RECEIPTS` (CMakeLists.txt:65); what the receipts pin is unverified (F). @@ -202,15 +202,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| 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:110-119) 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:303 | 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 | +| 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:110-119) 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:307 | 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:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2318; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2335, `:9996`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. 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:272. **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:1027-1029 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:2368; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:2385, `:10068`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2806. 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:272. **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:558) | 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:7253 | 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:7321 | 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) | -| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27061 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | +| i | `pf_pending_order_v1_t` Pine-named fields | `tv_carry_qty` pending_order_mirror.hpp:73, `pine_birth_reach` pending_order_mirror.hpp:135, `pine_exit_activation_owner_cycle_at_birth` pending_order_mirror.hpp:147-156, `pine_frozen_market_instruction_kind` pending_order_mirror.hpp:164-169; enumerated by `PF_PO_FIELD` pending_order_mirror.cpp:144-178; written only by `pine_birth_reach` pine_adapter.cpp:27063 | no change | Freeze (ABI). The neutral view arrives beside it (L7 / L13) | O (single-source) | | j | Pine-shaped lot flags | **Mostly done.** `market_pyramid_add` moved to the adapter side table the row asked for — `market_pyramid_adds_` src/source/pine_state_hash.cpp:692, whose own comment records that it was `PyramidEntry::market_pyramid_add` — and `ordinary_market_open`, `pooc_terminal_market_entry`, `ordinary_stop_open`, `bracket_slot_shadowed` and `entry_path_position` are all deleted. What is left on the lot is the intrabar-fill excursion mask, `skip_entry_bar_high` / `skip_entry_bar_low` engine.hpp:153, hashed (`skip_entry_bar_high` engine_state_hash.cpp:74) and, since R5 lane E6, written by no host at all: a host that owns lot excursions declares where its fill sat (`declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the Pine host did until R5 lane H-THIN left its excursions to the kernel's sampler) and the kernel derives the pair | HASH-VISIBLE | Move the live flag to an adapter side table keyed by `entry_incarnation` (the adapter already keeps `PlacementTable` pine_adapter.hpp:355-675) and fold it through `hash_source_extension`; delete the dead ones; update the hash-coverage waivers. Whether a kernel TU beyond the hash reads them is unverified (O) | F O S | | k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:672. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:511; the frozen C symbol remains source-owned. | HASH-VISIBLE (R5-8) | Keep the source setter in the adapter; the kernel toggle is the presence of `spec.margin` | F O | | l | Kernel decodes adapter strings | **Done.** `closed_trade_close_cause` engine_trade_accessors.cpp:158 branches on no string at all: it reads `Trade::open_at_end`, then the typed `execution::CloseCause` the closer recorded on the row, then `exit_from_bracket`. A kernel-originated liquidation or risk flatten states its own cause through the settling fill, so a bare host reads `3` / `4` where the retired string derivation answered `1`. The public C contract it implements is `strategy_closed_trade_close_cause` pineforge.h:1309 | HASH-VISIBLE (adds durable state) | Kernel `enum class CloseCause` recorded on the `Trade` at settlement (the kernel sets `MarginCall` when L4 liquidates; a source virtual covers adapter causes). C numbers unchanged. After L4 | 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:8116 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1836): 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:8184 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1843): 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,16 +281,16 @@ 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:755-770, `contribute_input` native_execution_consumer.cpp:9109, `seal_script` native_execution_consumer.cpp:9019) 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:7987), 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:755-770, `contribute_input` native_execution_consumer.cpp:9177, `seal_script` native_execution_consumer.cpp:9087) 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:8055), 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) | 1. **Spec:** `std::vector subscriptions` (Appendix A.5), hashed only when non-empty (digest like `native_intrabar_path_digest`). 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:956, *fresh*), so L6 adds a kernel-side twin instead of moving it. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1598-1682 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:956, *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:1889); `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:1896); `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". @@ -299,7 +299,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the **Status (L10): done.** `examples/native/` holds eighteen hosts, sixteen C++ and two C, built by `PINEFORGE_BUILD_EXAMPLES`; `include/pineforge/native_module.hpp` defines `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)`. The bullets below are the plan L10 worked from. -- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (then "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:508-509, with `native_market_example` runner/CMakeLists.txt:54). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). +- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (then "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:509-510, with `native_market_example` runner/CMakeLists.txt:54). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). - Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3237-3305) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:54, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:158. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). @@ -330,7 +330,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 80 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 80 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:733786ed81c9137223b98e1887e28d01afdb3c64ba5e8ca52ce5161f2caf3f75` c_abi.cpp:325-1013, `sha256:6327f1829019fdd172cd76a96e3d8c9c0af1610ad0dc9878640dda0ca21a686b` check_c_abi_runtime.py:20-81, c_abi.cpp:946, `sha256:5ff4c65a56410fe29b21cd68bc21ac87bdb34b77344e4f75f90efa8887fe48e8` c_abi.cpp:664-788, `sha256:051fe087eb9d30e9960dc4f0ab3f7cc837531ebeaa71b5871a3f1cdc22fb37c2` c_abi.cpp:566-641, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:545-562, `sha256:6a9ef4e688f33dc10ea8297d9eaa8144ea7923ef6ee6f8fe9ff20f6af4e9acb3` c_abi.cpp:881-917, c_abi.cpp:1013 | 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:115 and `add_library` CMakeLists.txt:158 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:158 | S (F, O agree in §2.i) | +| 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:115 and `add_library` CMakeLists.txt:159 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:159 | S (F, O agree in §2.i) | | 12 | Host constructors | `NativeStrategyHost` is a zero-argument C++ host with the callbacks and the request API; `PineStrategyHost` is the class that takes `CapAttachment` — do not attribute it to the native constructor | `NativeStrategyHost` native_host.hpp:845, `PineStrategyHost` pine_strategy_host.hpp:287 | S (single-source) | `docs/pages/native-engine.md` fixes, all applied (F unless noted). The page is cited by @@ -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:10429 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:10501 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:1919-1923). 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:1942-1946). 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,8 +413,8 @@ 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:2855, 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:6439) and `drain_applied_notifications` (native_execution_consumer.cpp:7122) 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) | +| 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:6507) and `drain_applied_notifications` (native_execution_consumer.cpp:7190) 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:1091-1175 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) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1423) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -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:5769). 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:5837). 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) | @@ -501,8 +501,8 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:881) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:239) and `configure_native` has +(pine_strategy_host.cpp:886) — after `prepare_native_begin` has already +projected the spec (pine_strategy_host.cpp:243) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads `max_drawdown=0 cap.active=0` for every risk probe. `NativeRunSpec::risk` is a @@ -518,17 +518,17 @@ 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:4381 +`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:4449 (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 | |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2859); the forbidden opposite entry is reshaped to `CloseOpposite` in the adapter's own terms branch | per-opening cap `allowed_open_directions` | (i)+(ii), **already re-lowered** | none to measure: the cap is the kernel's, the close-only reshaping is the retained TV policy | done before N12; nothing to move | -| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16491; the retired legacy rule is `git show ab9714be:src/source/pine_risk.cpp`, lines 115 and 2531) | `max_abs_units`: refuses a fill whose RESULTING book would exceed it | (iv) | `PS`: two-unit entries against 3 — adapter admits the second (live 2 < 3, book 4) and refuses the third; kernel refuses the second (`MaxAbsUnits` at bars 1, 2), book 2 | retained: a pre-fill-book gate is a TV emulation fact, not a generic cap | -| `strategy.risk.max_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17481, `update_risk_state` pine_adapter.cpp:17481): peak / running max drawdown at close marks, `>=` exact, percent of the current peak; the latch `risk_.halted` gates flat/same-side entries at precommit (`risk_.halted` pine_adapter.cpp:16483), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | -| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25721); gated immediately at precommit and latched at the close | `max_consecutive_loss_days`: a day's NET realized result settles the streak when the next day opens | (iv) | `MG11-a` (loss; win then loss; loss): adapter latches at day 3's first loss (4 rows), kernel's netted streak never reaches 2 (6 rows, no event). `MG11-b` (loss then smaller win, twice): adapter resets daily (5 rows), kernel blocks at day 3's open (4 rows, one event). No corpus probe declares the rule | retained: an order-dependent per-trade streak is not a generic day result | -| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:22490); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20856), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20888, `submit_intraday_loss_close` pine_adapter.cpp:20856), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24678); epsilon `1e-9·max(1,|limit|)`; ticket `"Close Position (Max intraday Loss)"` → `CloseCause::RiskLimit`; the block refuses ALL placements for the chart day (`entry`/`order`/`close`/`close_all`/`exit`, `intraday_loss_orders_blocked` pine_adapter.cpp:17476, `:17476`, `:17476`, `:17476`, `:17476`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14577) | `max_intraday_loss` at the three points; `FlattenAndBlock` closes `AsPresented` under `"__kernel_risk__"` / `"Risk limit"`, blocks openings only, leaves working orders | (ii) ticket, day basis + (iv) path check, placement scope, unbooked fill, epsilon | `MG12-a`: day 2's first bar 100→85→95 — adapter closes at the extreme (85, −1500, cause 4) and refuses the day's later entry, kernel measures 0 at the open and 500 at the close, never breaches, ends with 300 units. `MG12-b`: breach at a bar open inside the day — same money on both sides (100 @ 88, −1200) under different tickets; the adapter withdraws a resting limit entry, the kernel leaves it working and it fills on day 3. Both sides mark the day's opening equity at its first bar: a gap there is not an intraday loss for either | retained: the path-extreme check is the rule's substance and the kernel has no path evaluation point; a risk ticket field and a `PathAdverseExtreme` check kind would be generic additions (mirroring `NativeMarginModel`) but would still leave the placement scope, the unbooked fill and the epsilon | -| `strategy.risk.max_intraday_filled_orders` | `compat::pine::IntradayCap` + `IntradayOrderBudget` (243 + 94 lines): charged slots per risk day, quota transfer, POOC deferral, noop-market skip, the close at the fill price or the bar's better extreme (`post_dispatch`), latch → all placements denied (`cap_placement_denied` pine_adapter.cpp:17472); ticket `"Close Position (Max number of filled orders in one day)"` → `CloseCause::FillCap`; day = `session_trading_day_index` for a `HHMM-HHMM` session, else the chart day (`chart_day_key` pine_adapter.cpp:17378, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:16493; the retired legacy rule is `git show ab9714be:src/source/pine_risk.cpp`, lines 115 and 2531) | `max_abs_units`: refuses a fill whose RESULTING book would exceed it | (iv) | `PS`: two-unit entries against 3 — adapter admits the second (live 2 < 3, book 4) and refuses the third; kernel refuses the second (`MaxAbsUnits` at bars 1, 2), book 2 | retained: a pre-fill-book gate is a TV emulation fact, not a generic cap | +| `strategy.risk.max_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:17483, `update_risk_state` pine_adapter.cpp:17483): peak / running max drawdown at close marks, `>=` exact, percent of the current peak; the latch `risk_.halted` gates flat/same-side entries at precommit (`risk_.halted` pine_adapter.cpp:16485), never an opposite entry | `max_drawdown`: current drawdown vs percent-of-peak, at all three points; `BlockOpenings` refuses every opening | (ii) cadence + (iv) scope | `MG10-a`: bar 1 opens 600 under the entry and closes 100 under — kernel breaches at the open mark and refuses bar 2's add (book 100), adapter admits it (book 200). `MG10-b`: latched at bar 1's close, the adapter still reverses twice (`L→S` −600, `S→L`), the kernel refuses both reversals (`RiskLimit` at bars 2, 3) and holds. Corpus: `ta-closedtrades-risk-introspection-01` (20 % of peak) identical 1502/1502 rows with the kernel seeded — the rule never fires there | retained: the latched-reversal exemption cannot be composed on a kernel block; a close-only cadence knob alone would not close it | +| `strategy.risk.max_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1419-1427): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (`consecutive_loss_days` pine_adapter.cpp:25723); gated immediately at precommit and latched at the close | `max_consecutive_loss_days`: a day's NET realized result settles the streak when the next day opens | (iv) | `MG11-a` (loss; win then loss; loss): adapter latches at day 3's first loss (4 rows), kernel's netted streak never reaches 2 (6 rows, no event). `MG11-b` (loss then smaller win, twice): adapter resets daily (5 rows), kernel blocks at day 3's open (4 rows, one event). No corpus probe declares the rule | retained: an order-dependent per-trade streak is not a generic day result | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:22492); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20858), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:20890, `submit_intraday_loss_close` pine_adapter.cpp:20858), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24680); epsilon `1e-9·max(1,|limit|)`; ticket `"Close Position (Max intraday Loss)"` → `CloseCause::RiskLimit`; the block refuses ALL placements for the chart day (`entry`/`order`/`close`/`close_all`/`exit`, `intraday_loss_orders_blocked` pine_adapter.cpp:17478, `:17478`, `:17478`, `:17478`, `:17478`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14579) | `max_intraday_loss` at the three points; `FlattenAndBlock` closes `AsPresented` under `"__kernel_risk__"` / `"Risk limit"`, blocks openings only, leaves working orders | (ii) ticket, day basis + (iv) path check, placement scope, unbooked fill, epsilon | `MG12-a`: day 2's first bar 100→85→95 — adapter closes at the extreme (85, −1500, cause 4) and refuses the day's later entry, kernel measures 0 at the open and 500 at the close, never breaches, ends with 300 units. `MG12-b`: breach at a bar open inside the day — same money on both sides (100 @ 88, −1200) under different tickets; the adapter withdraws a resting limit entry, the kernel leaves it working and it fills on day 3. Both sides mark the day's opening equity at its first bar: a gap there is not an intraday loss for either | retained: the path-extreme check is the rule's substance and the kernel has no path evaluation point; a risk ticket field and a `PathAdverseExtreme` check kind would be generic additions (mirroring `NativeMarginModel`) but would still leave the placement scope, the unbooked fill and the epsilon | +| `strategy.risk.max_intraday_filled_orders` | `compat::pine::IntradayCap` + `IntradayOrderBudget` (243 + 94 lines): charged slots per risk day, quota transfer, POOC deferral, noop-market skip, the close at the fill price or the bar's better extreme (`post_dispatch`), latch → all placements denied (`cap_placement_denied` pine_adapter.cpp:17474); ticket `"Close Position (Max number of filled orders in one day)"` → `CloseCause::FillCap`; day = `session_trading_day_index` for a `HHMM-HHMM` session, else the chart day (`chart_day_key` pine_adapter.cpp:17380, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | **Proposed generic additions (not implemented — no consumer today).** A risk ticket (`label` / `comment` on `NativeRiskLimits`, folded only when set, @@ -557,8 +557,8 @@ Pine run needs the source change the patch makes, and the in-process pins remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:345, the kernel's `configure` at :354, -the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:881), and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:350, the kernel's `configure` at :359, +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:886), and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -851,7 +851,7 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:1982), after every applied execution once terminal (`record_applied_range_end`, :796), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:1987), after every applied execution once terminal (`record_applied_range_end`, :801), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:688, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | | **the excursion projection gate** | `excursion_range_end_projection_` suppressed the owner's exit-bar path fold for the projection | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) | retired by R5 lane H-THIN: the Pine host owns no lot excursion since, so no owner fold is left to suppress; `RE1` still reads the same magnitudes, now the kernel's | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | @@ -1133,10 +1133,10 @@ moving a single value, but not from inside lane F9's files: 1,339 rows each one callback old, an OCA re-price and a trail attach rows two callbacks old, and the row of an entry that filled at bar 1 is rewritten at bar 60, when its first exit is attached - (`has_full_entry_bracket` pine_adapter.cpp:11423). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:11425). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1965, `revive_brackets_after_margin` pine_adapter.cpp:2200, - `preserved_by_close_all` pine_adapter.cpp:14645), and + `preserved_by_close_all` pine_adapter.cpp:14647), and `admission::Journal::retain` market_admission.hpp:205 drops events from the middle of the journal. So "a row stops changing once its request is done" is false, and a cache resting on it would move a value. @@ -1224,13 +1224,13 @@ retires every leg of each exit row it names and drops such a parked stop (`retire_cancelled_exits` pine_adapter.cpp:2123), the revival skips a withdrawn exit, and K1 holds a withdrawn leg only while its origin is live or opened -- the one reader left is `origin_leg_consumed`, and it asks about such origins alone (`erase_retired_rows` -pine_adapter.cpp:3637). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:3639). Both shapes now cost ×1.89 to ×2.12 per doubling up to 64,000 bars and retain 3 rows. Row D was the kernel's half: the adapter enrolled every opening in its id's kernel roster (`cohort_add`) and never took one off, and `hash_cohorts` folds every member at every read. An origin whose request chain no longer works and that holds no open lot can never be bound again, so at every bar open that follows a command event the adapter takes -it off (`release_closed_cohort_origins` pine_adapter.cpp:4085); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:4087); a lot opened by a replace successor keeps every older member of its roster while it is open, because its root is not a public fact. The flip then costs ×1.99, ×2.01, ×2.02 per doubling, and its rosters end with one member where they @@ -1288,7 +1288,7 @@ engine.hpp:1667) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:634, `exit_comment` :763, `close_cause` :777-782, `exit_from_bracket` :1283 and `entry_incarnation` :1464-1472. +writes are `close_cause` pine_strategy_host.cpp:639, `exit_comment` :768, `close_cause` :782-787, `exit_from_bracket` :1288 and `entry_incarnation` :1469-1477. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1320,8 +1320,8 @@ the bars cost 3.8 times the CPU with a round trip every ten bars script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` pine_strategy_host.cpp:166). `capture_script_continuation_hash` -pine_strategy_host.cpp:527 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:608), and taking it is one full +pine_strategy_host.cpp:532 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:613), and taking it is one full continuation fold per run. Measured on the gated replay — `ReissueReplay` on 43,008 bars, `run()` process CPU, best of 5, three rounds — it is 0.2116 / 0.2137 / 0.2141 s with the capture and 0.1632 / 0.1614 / 0.1652 s with it @@ -1599,14 +1599,14 @@ 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:1919); 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:17944); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20222 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:3182, the COOF `at_fill` exception pine_adapter.cpp:8393) | 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:727, `coof_next_waypoint` pine_adapter.cpp:6947) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6439 and `drain_applied_notifications` (native_execution_consumer.cpp:7122); 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:22250) | 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:2786) | 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 | +| 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:1942); 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:17946); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20224 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:3184, the COOF `at_fill` exception pine_adapter.cpp:8395) | 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:727, `coof_next_waypoint` pine_adapter.cpp:6949) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:6507 and `drain_applied_notifications` (native_execution_consumer.cpp:7190); 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:22252) | 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:2791) | 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:384) 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 | -| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1811; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E8 | **HTF.** The evaluator machinery carries TV publication semantics (lookahead, boundary deferral, early close, `syminfo.type` branching engine_security.cpp:67-73). The feed store is TV-calibrated too: "W" / "M" from native daily bars, native stamps as the period partition (`set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:51-83, *fresh*). Two aggregators exist: script buckets use `native_calendar`, `request.security` uses `TimeframeAggregator`, whose completion rules are TV-pinned (timeframe.hpp:288-330) | Reuse risks changing the *order* of the Pine scheduler's pumps; a bare host inherits TV partition rules; native HTF may differ from Pine HTF on early-close sessions | Registration-gated pump; no change to `scheduler_feed_security_input` pine_strategy_host.cpp:1816; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | @@ -1623,7 +1623,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native when this plan was written (on this tree 543 `tests/*.cpp` name `source::PineStrategyHost`, 110 `NativeStrategyHost`) | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, security feeds and source setters, and tick input under an FX curve (any non-empty curve until audit lane N6) (`native-engine.md`, "Stream limitations (current)") | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | | P7 | **C ABI variant growth** | §2.iii hardening rules | S | -| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:881 → `set_risk_direction` pine_adapter.cpp:26463 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | +| P8 | **Unverified in the inputs:** whether `execution::LifecycleEffects` (execution.hpp:136-139) is ever non-empty from the Pine path, else `PlacementSnapshot::legs` (pine_adapter.hpp:364) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:886 → `set_risk_direction` pine_adapter.cpp:26465 (O); what the ABI receipts pin (F); the probe corpus itself lives outside this tree (F) | verify inside the lane that touches each | F O | ### 4.3 Open design questions (supervisor level) @@ -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:11995); 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:12072); 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 | @@ -1650,7 +1650,7 @@ All six are settled by what landed: U1 (native subscriptions take `lookahead` an | **U3** | **Do risk limits ship in v1?** | R5-9: in scope, not a blocker. S wanted them as a prerequisite; F accepts a toolkit stand-in; O defers. | | **U4** | **Stream-safe FX (FP6): in the roadmap or not?** It has no R5 lane. | F: out of scope here ("O7 clock"). S: its P10, an FX epoch consumed while flat, same curve in batch and stream. | | — | **U4, answered by audit lane N6.** In: the curve declared through `configure_native_fx_curve` is the FX epoch of the run, batch or stream; the account converts at the rate of each driver point's effective time, so the epoch is consumed by the walk itself, flat or not, and a confirmed-bar stream is the batch of the same bars. No `NativeFxEpoch` type and no `on_native_fx_epoch` callback were needed. Out, permanently: realtime rate ingestion (a new rate is a new run) and tick-driven input under a curve (its hooks and partial slots read a stale conversion clock — measured). | — | -| — | **N6 follow-ups (measured on the adapter; both closed by R5 lane F7).** (1) `apply_fx_open_margin_slice` (pine_adapter.cpp:5677) throws on a rate step over a carried position whose margin is not 100 %: a 2× long carried through a 1.0 → 1.5 step ends the whole run with status 1 and an empty report, where the same book without a series, or with a series that never steps, completes. The kernel's `FxRoll` point is the generic mechanism that case can re-lower onto. (2) `default_sizing_intent` (pine_adapter.cpp:3255) returns nullopt whenever any FX series is staged — even one point that restates the scalar rate — so an FX run silently loses the kernel `Sized` path R2b landed (measured: 1 kernel `Sized` request without a series, 0 with a one-point 1.0 series, same 50-unit book). **Closed by R5 lane F7.** (1) The throw is gone: the carried roll takes TradingView's broker-open checkpoint at any positive margin — its money at the open under the new rate and the side's own maintenance ratio, its lot-floored 4x restore, executed at the open before the step bar's script — and a survivor that is not a 1x long is re-checked over the rest of the bar by the kernel's liquidation. A 2x long or short of 15 at 100 on 1000 across 1 → 1.5 takes 6.666666666666667 at the open and keeps 8.33; at a 25 % side ratio it takes nothing; a constant curve never rolls (`tests/test_affordability_fx.cpp` F7 rows; G6's leveraged pin and its L0 oracle twin moved with an `expectation corrected:` note). The kernel's `FxRoll` point stays refused, measured (MG9). (2) `Sized` is withdrawn only where the kernel would convert at another rate than the source sized at — a curve step between the sub-bar open and the acceptance coordinate — so a one-point series lowers exactly as no series does, and on a stepped curve only the step's own bar keeps its host-resolved intent (`tests/test_adapter_sizing_relower.cpp` case 12). | — | +| — | **N6 follow-ups (measured on the adapter; both closed by R5 lane F7).** (1) `apply_fx_open_margin_slice` (pine_adapter.cpp:5679) throws on a rate step over a carried position whose margin is not 100 %: a 2× long carried through a 1.0 → 1.5 step ends the whole run with status 1 and an empty report, where the same book without a series, or with a series that never steps, completes. The kernel's `FxRoll` point is the generic mechanism that case can re-lower onto. (2) `default_sizing_intent` (pine_adapter.cpp:3257) returns nullopt whenever any FX series is staged — even one point that restates the scalar rate — so an FX run silently loses the kernel `Sized` path R2b landed (measured: 1 kernel `Sized` request without a series, 0 with a one-point 1.0 series, same 50-unit book). **Closed by R5 lane F7.** (1) The throw is gone: the carried roll takes TradingView's broker-open checkpoint at any positive margin — its money at the open under the new rate and the side's own maintenance ratio, its lot-floored 4x restore, executed at the open before the step bar's script — and a survivor that is not a 1x long is re-checked over the rest of the bar by the kernel's liquidation. A 2x long or short of 15 at 100 on 1000 across 1 → 1.5 takes 6.666666666666667 at the open and keeps 8.33; at a 25 % side ratio it takes nothing; a constant curve never rolls (`tests/test_affordability_fx.cpp` F7 rows; G6's leveraged pin and its L0 oracle twin moved with an `expectation corrected:` note). The kernel's `FxRoll` point stays refused, measured (MG9). (2) `Sized` is withdrawn only where the kernel would convert at another rate than the source sized at — a curve step between the sub-bar open and the acceptance coordinate — so a one-point series lowers exactly as no series does, and on a stepped curve only the step's own bar keeps its host-resolved intent (`tests/test_adapter_sizing_relower.cpp` case 12). | — | | **U5** | **Open-bar view default.** Should `OpenOnly` be the default for hosts that are not `LegacyTolerant`? | F: safer for users, but a behaviour change for the ~20 existing native tests; default `Complete` proposed. | | **U6** | **TA comparison band.** Is TradingView's 1e-10 tie-breaking a feature of the TA library, or a default to make configurable? | F, O: rename only; a switch changes indicator outputs and needs its own lane. S: separate policies, hash the policy identity. | @@ -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:8081, :8081, :8081, :8081) 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:8149, :8149, :8149, :8149) 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:2855), 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, `seal_script` native_execution_consumer.cpp:9019), `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:9087), `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:732; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5740). 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:732; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:5808). 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:20209). 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:3280), 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:20211). 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:3348), 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:3604 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:3672 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:7866-7878) 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:6439). `born_on_remaining_path` (native_execution_consumer.cpp:6439) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1961-1991) 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:1283-1290, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7712). `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:3224); 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:7934-7946) 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:6507). `born_on_remaining_path` (native_execution_consumer.cpp:6507) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1961-1991) 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:1283-1290, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:7780). `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:3292); 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:7440), `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:4167-4189, `execute_current` path native_execution_consumer.cpp:7508), `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) @@ -1836,8 +1836,8 @@ 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:17310-17318); 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:3604 and the per-point walk), `native_order.hpp`, the state hash. +- 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:17312-17320); 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:3672 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 fd9c33a3f..f55e931e5 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:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6446`), 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:321`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:6448`), 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 | @@ -183,7 +183,7 @@ them for the record only. "Linux aarch64" is the maintainers' Cortex-A725 host. | Compact journal records and a history ring buffer (L3) | Smaller command records and a ring buffer for the request core's history. `WorkingRequestCore::history` (`include/pineforge/native_order.hpp:1709`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2280`) inside `native_order_v7`, so both are a layout and API change | L3, Linux aarch64 (Cortex-A725), `perf` call graphs, inclusive shares: at most 3.68 % of bracket, 11.99 % of re-issue, 4.26 % of market and 3.42 % of the 14 probes (retiring the history alone: 1.64, 4.51, 2.53 and 1.00 %); only part is realisable, since records must still be written | "R5 L3 DONE + ACCEPTED 2026-09-24 22:10 Taipei", ruling 1: "DEFERRED to the PERF-D3 structural plan"; TRIAGE1 §D: "compact journal records" | Lane L3's code, merged by INT22 (#285); its profiles are unpublished | A `native_order` layout epoch | | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4887`) and again in `apply_execution` (`src/native_order.cpp:4910`); 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:17351`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22250`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22487`) 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:17335`) | 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 | +| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:17353`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22252`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22489`) 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:17337`) | 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:496`). 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:620`) 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 | @@ -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:3243-3248`) returns +`reserve_driver_log` (`src/native_execution_consumer.cpp:3311-3316`) returns unless the retention is Full, and `presize_logs` -(`src/native_execution_consumer.cpp:10278-10289`) sizes no journal under Window. +(`src/native_execution_consumer.cpp:10350-10361`) 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:2996`). K4 still @@ -210,7 +210,7 @@ or more, and the same run under Full holds 66,000 points in one block of **PERF-L4 reads the receipts about three times a bar, not once.** PERF-L4's brief asked it to "read the receipts ONCE per bar instead of three times". The lane gated the read instead: `observe_terminal_receipts` -(`src/source/pine_adapter.cpp:6446`) returns at its receipt gate when no +(`src/source/pine_adapter.cpp:6448`) returns at its receipt gate when no command lies above its cursor. `every_gate_is_taken_both_ways` (`tests/test_adapter_quiet_bar.cpp:1061`) prints `gate ReceiptRead asked 40996 quiet 33552 live 7444`, against `asked 13248` diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index f7706c690..113801c7d 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -261,7 +261,7 @@ decision, and removing one is a regression: **kernel** — the Pine-agnostic half of the engine: matching, fills, sizing, margin, settlement, indicators, calendars. Buildable alone as -`PineForge::kernel` CMakeLists.txt:159 with `PINEFORGE_BUILD_SOURCE_LAYER` +`PineForge::kernel` CMakeLists.txt:160 with `PINEFORGE_BUILD_SOURCE_LAYER` CMakeLists.txt:43 off. **adapter** — `src/source/` and `src/compat/pine/`: the TradingView-parity diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index acedc6c43..3c158c39a 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:10429-10471`), which admits the begin, checks the +(`native_execution_consumer.cpp:10501-10543`), 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 @@ -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:6439-6443`). Requests accepted before the +`native_execution_consumer.cpp:6507-6511`). 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:7955-7957`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:8023-8025`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:7428`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:7496`), 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 @@ -1496,7 +1496,7 @@ adapter answers `margin_check_allowed` with TradingView's scheduling — which includes the post-exit re-size: when a priced bracket leg of the script bar fills, the slice resting at that bar's adverse extreme was sized on the pre-exit book, and the legacy broker cancelled and re-scheduled it there -(`margin_check_allowed` `pine_adapter.cpp:17830-17863`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:17832-17865`), so the adapter admits the kernel's own point for that driver point while (and only while) a slice rests — `resolve_margin_requirement` with its ten-significant-digit money and @@ -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:8841`), so +(`record_script_report_point`, `native_execution_consumer.cpp:8909`), 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:8925`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:8993`). 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 @@ -3150,7 +3150,7 @@ These are existing refusals, not implied future features: - In-session gaps on a realtime stream, and in a `Canonical` stream warmup - Source `calc_on_every_tick` / `calc_on_order_fills` enabled (the runner rejects an explicit true override, and the Pine host refuses a stream begin - with `calc_on_order_fills`, `pine_strategy_host.cpp:291-294`). This is a + with `calc_on_order_fills`, `pine_strategy_host.cpp:295-298`). This is a **source-route** refusal, not a limit on the native hooks: `on_native_tick` and `on_native_applied` are delivered on a stream, and a native host's own `NativeRunSpec::calculation` is accepted there, where `EveryModeledPoint` @@ -4550,7 +4550,7 @@ Nor is there a build-level one. The two source sets are disjoint: `PINEFORGE_SOURCE_LAYER_SOURCES` (`CMakeLists.txt:91`) holds all six `src/compat/pine/` units and all twelve `src/source/` ones, and `PINEFORGE_KERNEL_SOURCES` (`CMakeLists.txt:115`) holds the kernel's own -thirty-five, which are what `add_library` (`CMakeLists.txt:158`) compiles into +thirty-five, which are what `add_library` (`CMakeLists.txt:159`) compiles into `pineforge_kernel`. `libpineforge.a` still carries both sets when `PINEFORGE_BUILD_SOURCE_LAYER` is ON, which is the default; the kernel archive exists either way. The installed-header closure is clean too, which the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index e6d1f192a..0d72af7ad 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -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:1683. 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:7008-7012. 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:8525-8526, 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:3632). 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, the private test `test_pyramiding_count_differential`). 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:7076-7080. 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:8593-8594, 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:3700). 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, the private test `test_pyramiding_count_differential`). 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. | @@ -296,7 +296,7 @@ is `strategy_native_submit_v1` native_c_api.h:2742 over a | `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:3621. | +| `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:3689. | | `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. | @@ -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:3621. | +| `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:3689. | | `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. | @@ -463,7 +463,7 @@ part both ways (the private test `test_adapter_margin_schedule_differential`, M1 | `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 (`born_on_remaining_path` native_execution_consumer.cpp:6439-6443). | +| 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:6507-6511). | | 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. | @@ -1025,7 +1025,7 @@ cmake -S . -B build-kernel -DPINEFORGE_BUILD_SOURCE_LAYER=OFF -DPINEFORGE_BUILD_ ``` `PINEFORGE_BUILD_SOURCE_LAYER` CMakeLists.txt:43 OFF leaves `libpineforge.a` -holding exactly the objects of `PineForge::kernel` CMakeLists.txt:159, and +holding exactly the objects of `PineForge::kernel` CMakeLists.txt:160, and every example already links that target, so a source-layer symbol reaching an example is a link error in the default build too. diff --git a/include/pineforge/native_bar_views.hpp b/include/pineforge/native_bar_views.hpp index 5254c78c6..8a2034429 100644 --- a/include/pineforge/native_bar_views.hpp +++ b/include/pineforge/native_bar_views.hpp @@ -4,9 +4,13 @@ // This separate namespace is the side channel; native_run_spec_v3 and // native_host.hpp keep their existing layouts (2.2:339-345; 6:1328-1329). #include +#include #include #include +#include +#include +#include namespace pineforge { inline namespace native_bar_views_v1 { @@ -28,39 +32,179 @@ struct NativeInstrumentFeedView { const std::int64_t* close_ms; }; -/// Lower-path side-channel type named by 2.2:350-351 and section 5:1237-1262. -/// Operations in DESIGN 2.2:350-351 count and visit the whole path, -/// select a segment, and return the next bar after it. Counting and visiting -/// re-walk the path without retaining it; the segment supplies the current -/// script bar's half-open range. -/// Its buffer stays fixed for the whole script bar, until the next one. -/// Look-ahead uses a separate cursor/one-bar buffer and never overwrites -/// that segment, even across empty chart bars (5:1253-1257). -/// -/// OPEN V2: DESIGN names these operations but gives no typed signatures, -/// visitor protocol or dispatch/error model. Only the class is forward- -/// declared here; no speculative vtable or function signature is frozen. +/// RESOLVED V2: completed below with the protocol approved by CORE-TOP's +/// rev 6 amendment (10-18 19:24), proposal plus required changes 1-9. +/// DESIGN coordinates use the baseline identified in the contract map. class NativeLowerPathSource; -// Binding obligations for L15a (2.2:352-378): stage before begin with -// the staging operations specified in DESIGN 2.2:352-353, then drop -// staged state with the run. Keep NativeRunSpec metadata and empty owning -// bar vectors for view-backed entries. All reads must use the one accessor -// that returns the staged view when present, including validation and both -// cached/uncached digests, never just the kernel delivery loop. -// -// Digests fold the same count, bars and closes in the same order, with the -// same quiet-NaN canonicalization; hashed host counts read the view's count -// too (2.2:372-375). No digest/continuation identity changes are permitted. -// L15a adds this header and its source to the native ownership guard and -// adds the native_bar_views_v1 namespace check (2.2:376-378). -// -// Source-guard read-set: docs/design/cr150-contracts.md, "L15a read inventory" -// (2.2:357-371; 5:1221-1228). That inventory includes delivery, validation, -// counts, digests, store upkeep and the lower path, with DESIGN source refs. -// OPEN V1: staging signatures, feed binding/lookup and the distinction -// between an absent staged view and a present empty feed are unspecified. -// No consumer/host/frozen-header declaration is changed to guess them. +/// Effective feed data (DESIGN 2.2:354-375; RESOLVED V1). Owned feeds keep +/// their independently observable close count for validation and the digest's +/// missing-close zero fallback. A borrowed feed has close_count == bars.size. +struct NativeInstrumentFeedReadView { + NativeBarView bars; + const std::int64_t* close_ms; + std::size_t close_count; +}; + +/// Explicit read context (RESOLVED V1, required changes 1-4). Zero feed count +/// means owned vectors for every feed; the table pointer is then ignored. +/// Otherwise the nonnull table count equals the spec's metadata count, and +/// slot i binds entry i. Null slot: use owned vectors. Nonnull slot: present +/// borrowed data, including {{nullptr,0},nullptr} for an installed empty feed; +/// it never falls back to owned data. No metadata entry means absent. +/// Metadata/columns stay in NativeInstrumentFeed, with empty owning bar/close +/// vectors for a borrowed slot. Mixed owned/borrowed slots are allowed. +/// Nonempty borrowed arrays are nonnull, immutable, and have bars.size rows. +/// +/// Consumer staging copies descriptors and the table, not the arrays. Caller +/// descriptors/table may die after staging; arrays live through every use of +/// the accepted binding, through the public run exit. Installed-feed owners +/// also keep arrays alive until replacement, clearing or strategy_free. +/// The active query exposes no pending slot. Context pointers are borrowed +/// until replacement/detachment/destruction; the segment is empty until the +/// kernel selects one. The const source prevents readers selecting a segment. +/// +/// At every public run exit, including abort/error, detach tables/source and +/// segment, releasing borrowed pointers after retaining scalar counts/content +/// digests for identity. A detached Ready binding is never admitted: that is +/// engine_invariant. A lower-path source bound at stream_begin is likewise +/// engine_invariant. Staging/query are private consumer members, not functions +/// taking a public host (required changes 1-2). +struct NativeBarViews { + const NativeInstrumentFeedView* const* instrument_feeds = nullptr; + std::size_t instrument_feed_count = 0; + const NativeLowerPathSource* lower_path = nullptr; + NativeBarView lower_segment{nullptr, 0}; +}; + +/// Common accessor (DESIGN 2.2:355-375). Always select a nonnull override, +/// including a present-empty one; otherwise expose owned data and its actual +/// close count. No padding/copying. The caller MUST pass its slot or nullptr +/// explicitly: no default argument (required change 3). +NativeInstrumentFeedReadView native_instrument_feed_view( + const NativeInstrumentFeed& owned, + const NativeInstrumentFeedView* borrowed) noexcept; + +/// Existing full validation and zero-fee normalization over effective data. +/// At binding, table/spec count mismatch or nonempty null bars reports +/// {InvalidInstrumentFeedBar, InstrumentFeedBars}; nonempty null closes +/// reports {InvalidInstrumentFeedClose, InstrumentFeedClose}. A borrowed slot +/// with nonempty owning bars/closes reports the corresponding bar/close pair. +/// Existing key/timeframe/duplicate, bar value/order, close length/bounds and +/// column checks keep their order and typed calendar/allocation catches. +/// A source requires lower_tf metadata with empty owning lower bars; otherwise +/// report {InvalidIntrabarPath, IntrabarTimeframe}. No new C failure code. +/// +/// Also route the adapter's projection validation through this overload, +/// using the context built from its store. The host stages after lending feed +/// bytes and before projection (required change 5). Pending state is not read +/// through the consumer's active query. Context-free v3 APIs stay unchanged. +NativeRunSpecValidation normalize_native_run_spec( + NativeRunSpec& spec, const NativeBarViews& views) noexcept; + +/// The same feed-only validation over effective data, retaining all existing +/// refusal ordering and typed catches (DESIGN 2.2:361-363). +NativeRunSpecValidation validate_native_instrument_feeds( + const std::vector& feeds, + const NativeBarViews& views) noexcept; + +/// The same feeds-first, auxiliary-feed, then subscription judgement over +/// effective data, also at both begin declarations (DESIGN 2.2:361-363). +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, + bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& instrument_feeds, + const NativeBarViews& views) noexcept; + +/// Same count, metadata, ordered OHLCV/timestamp/close/columns fold and feed +/// NaN canonicalization (DESIGN 2.2:364-375). Valid live descriptors are a +/// precondition; owned close lengths remain total, missing closes fold zero. +std::uint64_t native_instrument_feeds_digest( + const std::vector& feeds, + const NativeBarViews& views) noexcept; + +/// Portable spec fold with an explicit context, sharing the existing fold +/// and relative run base; never infer external storage through a registry. +/// May re-walk a source and propagate exceptions; never invent a failure hash. +/// The one-argument v3 form remains the owned-value API. +/// +/// DESIGN 5 timing amendment (required change 8): configure takes immutable +/// feed/lower component digests eagerly after normalization succeeds and +/// before its first member write. Lower-walk exceptions propagate unchanged; +/// nothing is published, and they are NOT converted into validation refusals. +/// Keep these components through combined-cache invalidation and detachment, +/// so the noexcept continuation query never walks released input. The outer +/// spec fold may stay lazy; standalone context overloads may walk and throw. +std::uint64_t native_run_spec_digest( + const NativeRunSpec& spec, const NativeBarViews& views); + +/// Four-operation source (DESIGN 2.2:350-351; 5:1237-1262, as amended). +/// Null source means owned-vector fallback; a nonnull source is present even +/// when count() == 0. The consumer exclusively owns the provider from applied +/// configure to detachment at public run exit. Immutable borrowed input lives +/// through that exit; destruction must not read it. Calls are serialized, with +/// no concurrent/reentrant walks. Concrete factories/policy/storage stay L16's. +/// Every operation exposes the same complete logical path. Whole-input fallback +/// is decided before emitting any aggregated prefix; no mixed prefix/suffix. +class NativeLowerPathSource { +public: + /// Synchronous ordered callback; bar lives for this call only. true + /// continues, false stops normally. Context may be null; callback is + /// neither retained nor allowed to throw. + using Visitor = bool (*)(void* context, const Bar& bar) noexcept; + + virtual ~NativeLowerPathSource() noexcept = default; + + /// Fixed lifetime row count, including zero (required change 7). The + /// first call may walk or use the construction pre-scan; subsequent calls + /// return the cached scalar in O(1). No segment/look-ahead invalidation. + /// Operational failure throws. Overflow throws coded std::overflow_error, + /// run_options_rejected with option intrabar_path, never a wrapped count. + virtual std::size_t count() const = 0; + + /// Fresh whole-path walk, true only if complete and false only for visitor + /// stop. Null visitor is coded engine_invariant / std::logic_error. Source + /// failures throw. Never overwrite active segment/look-ahead, including + /// when a digest is read inside a callback. + virtual bool for_each(Visitor visitor, void* context) const = 0; + + /// Ordered contiguous rows with begin_ms <= timestamp < end_ms. Equal + /// bounds give an empty view; reversed bounds are coded engine_invariant + /// / std::logic_error. Each bound is non-decreasing across calls; windows + /// may repeat OR overlap, including overlap rows. A decreasing bound is + /// the same invariant error (required change 6). Repeats return the same + /// active storage. Replace a window only after its script bar finishes. + /// Prepare look-ahead here; failures expose no partial new segment. + virtual NativeBarView segment( + std::int64_t begin_ms, std::int64_t end_ms) = 0; + + /// Null before the first segment; otherwise the first path bar at/after + /// its end, or null at exhaustion, including for an empty segment. The + /// separate one-bar buffer stays fixed until a different segment is + /// selected. It may skip multiple empty chart bars; repeated queries do + /// not advance or overwrite the active segment. Within a segment, use its + /// next contiguous row; only the last row needs this look-ahead. + virtual const Bar* next_after_segment() const noexcept = 0; +}; + +/// Null delegates to the vector digest; nonnull replaces only lower count/ +/// walk. Preserve variant/metadata and raw double bits (no NaN normalization), +/// folding count before visitation. Unexpected visitor stop or count/visit +/// mismatch is coded engine_invariant; operational failures propagate, never +/// a sentinel digest. Configure-time handling is the eager rule above. +/// Preflight still owns lower-bar validation and the INT_MAX refusal. Runtime +/// segment-generation failures are coded run_options_rejected, option +/// intrabar_path; allocation exceptions take the existing run-entry handler. +std::uint64_t native_intrabar_path_digest( + const IntrabarPath& path, + const NativeLowerPathSource* source); + +// RESOLVED V1/V2 reader and guard work remains L15a/L15b/L16's implementation: +// the contract map lists all reads, including projection validation (change 5). +// Add this header/source and its namespace check to the native ownership guard, +// and pin the context spec-digest sibling block beside the existing fold. } // inline namespace native_bar_views_v1 } // namespace pineforge diff --git a/scripts/check_native_cpp_versions.py b/scripts/check_native_cpp_versions.py index 0c143da21..e166ec219 100644 --- a/scripts/check_native_cpp_versions.py +++ b/scripts/check_native_cpp_versions.py @@ -37,12 +37,194 @@ "include/pineforge/native_order_identity.hpp", "include/pineforge/native_fx_curve.hpp", "src/native_fx_curve.cpp", + 'include/pineforge/native_bar_views.hpp', + 'src/native_bar_views.cpp', + 'src/source/pine_security_eval.cpp', + 'src/source/pine_strategy_host.cpp', + 'src/source/pine_adapter.cpp', + 'src/source/pine_scheduler_native.cpp', + 'src/source/magnifier_intrabars.cpp', ) NATIVE_FX_CURVE_HEADER = "include/pineforge/native_fx_curve.hpp" NATIVE_FX_CURVE_SOURCE = "src/native_fx_curve.cpp" NATIVE_FX_CURVE_NAMESPACE = "native_fx_curve_v1" +# CR-150: approved v1 sibling bridges, as narrow as the original v3 digest +# exception. Every other definition stays owned by its existing epoch. +NATIVE_BAR_VIEWS_HEADER = 'include/pineforge/native_bar_views.hpp' +NATIVE_BAR_VIEWS_SOURCE = 'src/native_bar_views.cpp' +NATIVE_BAR_VIEWS_NAMESPACE = 'native_bar_views_v1' +SPEC_VIEWS_BRIDGE = """inline namespace native_bar_views_v1 { + +NativeRunSpecValidation normalize_native_run_spec( + NativeRunSpec& spec, const NativeBarViews& views) noexcept { + const auto judged = native_run_spec_v3::validate_run_spec_views(spec, views); + if (!judged) return judged; + if (spec.fee_value == 0.0) spec.fee_value = 0.0; + return {}; +} + +NativeRunSpecValidation validate_native_instrument_feeds( + const std::vector& feeds, const NativeBarViews& views) noexcept { + try { + return native_run_spec_v3::instrument_feed_shapes(feeds, views); + } catch (const std::bad_alloc&) { + return {NativeRunSpecError::AllocationFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } catch (...) { + return {NativeRunSpecError::CalendarFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } +} + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::validate_subscription_views( + subscriptions, input_tf, timeframe_undetected, auxiliary_feed, feeds, views); +} + +std::uint64_t native_instrument_feeds_digest( + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::digest_instrument_feed_views(feeds, views); +} + +} // inline namespace native_bar_views_v1 +""" +CONSUMER_VIEWS_BRIDGE = """inline namespace native_bar_views_v1 { + +std::uint64_t native_run_spec_digest(const NativeRunSpec& spec, const NativeBarViews& views) { + NativeExecutionConsumer::SpecBarDigests digests; + digests.intrabar = native_bar_views_v1::native_intrabar_path_digest(spec.intrabar, views.lower_path); + if (!spec.subscriptions.empty()) + digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); + if (spec.auxiliary_feed) + digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); + if (!spec.instrument_feeds.empty()) + digests.instruments = native_bar_views_v1::native_instrument_feeds_digest(spec.instrument_feeds, views); + Fnv f; + f.run_base = spec.identity.run_number; + hash_spec(f, spec, &digests); + return f.h; +} + +} // inline namespace native_bar_views_v1 +""" + +# Every .bars/.close_ms and ->bars/->close_ms access in the named read set. +# Storage writes are included deliberately. L15b/L16 update their rows when +# introducing a caller/provider; an unlisted access fails before ABI fixtures. +BAR_VIEW_READS = { + 'src/native_bar_views.cpp': ( + 'return {borrowed->bars, borrowed->close_ms, borrowed->bars.size};', + 'return {{owned.bars.data(), owned.bars.size()},', + 'owned.close_ms.data(), owned.close_ms.size()};', + 'if (lower == nullptr || !lower->bars.empty())', + ), + 'src/native_run_spec.cpp': ( + 'const auto bars = data.bars;', + 'if ((borrowed != nullptr && !feed.bars.empty())', + 'if ((borrowed != nullptr && !feed.close_ms.empty())', + '|| (data.close_count != 0 && data.close_ms == nullptr))', + 'if (data.close_ms[i] <= bars.data[i].timestamp', + '|| (i + 1 < bars.size && data.close_ms[i] > bars.data[i + 1].timestamp)) {', + 'for (std::size_t i = 0; i < feed->bars.size(); ++i) {', + 'if (!native_bar_structurally_valid(feed->bars[i])) {', + 'if (i > 0 && feed->bars[i].timestamp <= feed->bars[i - 1].timestamp) {', + '&& (spec.intrabar.lower() == nullptr || !spec.intrabar.lower()->bars.empty()))', + 'u(lower->bars.size());', + 'for (const auto& bar : lower->bars) {', + 'u(feed.bars.size());', + 'for (const auto& bar : feed.bars) {', + 'u(data.bars.size);', + 'for (std::size_t k = 0; k < data.bars.size; ++k) {', + 'const Bar& bar = data.bars.data[k];', + 'i(k < data.close_count ? data.close_ms[k] : 0);', + ), + 'src/native_execution_consumer.hpp': ( + ), + 'src/native_execution_consumer.cpp': ( + 'begin_bars_ = args.bars;', + 'descriptor->bars.data = nullptr;', + 'descriptor->close_ms = nullptr;', + 'if (feed.seconds == kSecPerDay && !feed.bars.empty()) {', + 'daily = &feed.bars;', + 'if (!intrabar_tf_ || lower.bars.size() > static_cast(std::numeric_limits::max())) {', + 'path_spec, lower.bars.empty() ? nullptr : lower.bars.data(),', + 'static_cast(lower.bars.size()), NativeInputPolicy::Batch);', + 'lower->bars.begin(), lower->bars.end(), begin,', + 'for (; candidate != lower->bars.end() && candidate->timestamp < end; ++candidate) {', + 'spec && spec->auxiliary_feed ? spec->auxiliary_feed->bars.size() : 0;', + 'const auto& declared = spec_ptr()->auxiliary_feed->bars;', + 'const std::size_t total = feed.bars.size;', + '? feed.bars.data[at].timestamp <= horizon.open_ms', + ': feed.close_ms[at] <= horizon.last_traded_close_ms;', + 'subscription.latest = feed.bars.data[at];', + 'context.interval.open_ms = feed.bars.data[at].timestamp;', + 'context.interval.eligible_open_ms = feed.bars.data[at].timestamp;', + 'context.interval.last_traded_close_ms = feed.close_ms[at];', + 'context.interval.next_period_open_ms = feed.close_ms[at];', + 'at + 1 < total ? feed.bars.data[at + 1].timestamp : feed.close_ms[at];', + 'if (!invoke_timeframe_callback(engine, feed.bars.data[at], context)) return false;', + ), + 'src/source/pine_security_eval.cpp': ( + 'feed.bars = held.bars;', + 'feed.close_ms = held.close_ms;', + 'site.close_ms = 0;', + 'site.close_ms = close_ms;', + 'while (static_cast(site.delivered) < feed.bars.size) {', + 'const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp', + ': feed.close_ms[at] <= chart_close;', + 'run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]);', + 'feed.bars.assign(bars, bars + n);', + 'feed.close_ms.assign(close_ms, close_ms + n);', + 'if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr))', + '+ " values for " + std::to_string(data.bars.size) + " bars");', + 'f.u(native_instrument_feed_view(feed, nullptr).bars.size);', + ), + 'src/source/pine_strategy_host.cpp': ( + 'if (args.n > 0 && args.bars == nullptr) {', + 'const Bar& bar = args.bars[i];', + 'const std::int64_t previous = args.bars[i - 1].timestamp;', + '&& (!std::isfinite(args.bars[args.n - 1].close)', + '|| args.bars[args.n - 1].close <= 0.0)) {', + 'lent[i].bars.swap(store[i].bars);', + 'lent[i].close_ms.swap(store[i].close_ms);', + 'store[i].bars.swap(lent[i].bars);', + 'store[i].close_ms.swap(lent[i].close_ms);', + '{feed.instrument, feed.tf, data.bars.size, feed.columns.size()});', + 'if (effective_input.empty() && args.n >= 2 && args.bars != nullptr)', + 'effective_input = detect_timeframe(args.bars, args.n);', + ), + 'src/source/pine_adapter.cpp': ( + 'if (effective_input.empty() && args.n >= 2 && args.bars != nullptr) {', + 'effective_input = detect_timeframe(args.bars, args.n);', + 'if (args.bars && args.n > 0) {', + 'path.bars = tradingview_magnifier_bars(', + 'args.bars, args.n, spec.input_tf, spec.script_tf, spec.session,', + 'const bool inactive_sampler_path = inert != nullptr && inert->bars.empty()', + ), + 'src/source/pine_scheduler_native.cpp': ( + 'if (args.bars && args.n > 0) next.bars.assign(args.bars, args.bars + args.n);', + 'input_script_completes_.assign(retained_.bars.size(), 1U);', + 'input_script_boundary_completes_.assign(retained_.bars.size(), 0U);', + 'retained_.bars.data(), retained_.bars.size(), input_script_completes_,', + 'selected_executed_source_bars(host, state.spec, window, retained_.bars);', + 'host.scheduler_prepare_script_run(retained_.bars, static_eligible,', + 'host.scheduler_prepare_security_sequence(retained_.bars);', + 'lower->bars.begin(), lower->bars.end(), context.sub_bar_open_ms,', + 'if (found == lower->bars.end() || found->timestamp != context.sub_bar_open_ms)', + 'const auto& path = spec->intrabar.lower()->bars;', + '&& context.input_index + 1 < static_cast(retained_.bars.size())) {', + 'next_input_ms = retained_.bars[static_cast(context.input_index + 1)].timestamp;', + '&& !retained_.bars.empty() && calendar_period_for(retained_.script_tf) == CalendarPeriod::DAY', + 'const auto next_stamp = session_period_open_ms(retained_.bars.back().timestamp + 86400000LL,', + ), + 'src/source/magnifier_intrabars.cpp': ( + ), +} + DRIVER_FORWARD = ( "inline namespace native_run_spec_v3 { struct NativeRunSpec; }" ) @@ -165,7 +347,10 @@ def consumer_epoch_source(files): "native_run_spec_v3 spec fold, seeded like continuation_hash()") if "native_run_spec_digest" not in files["include/pineforge/native_run_spec.hpp"]: raise ValueError("native_run_spec_digest must be declared in native_run_spec_v3") - epoch = versioned(text.replace(CONSUMER_SPEC_DIGEST, "", 1), + if text.count(CONSUMER_VIEWS_BRIDGE) != 1: + raise ValueError('context spec digest must preserve its exact v1 bridge') + epoch = versioned(text.replace(CONSUMER_SPEC_DIGEST, "", 1) + .replace(CONSUMER_VIEWS_BRIDGE, "", 1), "pineforge", "engine_script_run_v19") if "native_run_spec_digest" in epoch: raise ValueError("native_run_spec_digest does not belong to engine_script_run_v19") @@ -325,8 +510,97 @@ def authenticate_historical_host_manifests(root=ROOT, providers=PROVIDERS): return manifests +def check_native_bar_views(files): + header = clean(files[NATIVE_BAR_VIEWS_HEADER]) + value = versioned(header, 'pineforge', NATIVE_BAR_VIEWS_NAMESPACE) + same_names(header, value, TYPE_DEF, 'bar-view public type', NATIVE_BAR_VIEWS_NAMESPACE) + if namespace_functions(header) != namespace_functions(value): + raise ValueError('bar-view functions must belong to native_bar_views_v1') + require(value, ('NativeBarView', 'NativeInstrumentFeedView', + 'NativeInstrumentFeedReadView', 'NativeBarViews', 'NativeLowerPathSource'), + NATIVE_BAR_VIEWS_NAMESPACE, r'\b(?:struct|class)\s+NAME\s*\{') + shapes = ( + ('NativeBarView', 'constBar*data;std::size_tsize;'), + ('NativeInstrumentFeedView', 'NativeBarViewbars;conststd::int64_t*close_ms;'), + ('NativeInstrumentFeedReadView', + 'NativeBarViewbars;conststd::int64_t*close_ms;std::size_tclose_count;'), + ('NativeBarViews', 'constNativeInstrumentFeedView*const*instrument_feeds=nullptr;' + 'std::size_tinstrument_feed_count=0;constNativeLowerPathSource*lower_path=nullptr;' + 'NativeBarViewlower_segment{nullptr,0};'), + ) + for name, expected in shapes: + shape = body(value, r'\bstruct\s+' + name + r'\s*\{', name) + if re.sub(r'\s+', '', shape) != expected: + raise ValueError(name + ' must preserve its frozen v1 fields') + functions = ('native_instrument_feed_view', 'normalize_native_run_spec', + 'validate_native_instrument_feeds', 'validate_native_timeframe_subscriptions', + 'native_instrument_feeds_digest', 'native_run_spec_digest', + 'native_intrabar_path_digest') + require_namespace_functions(value, functions, NATIVE_BAR_VIEWS_NAMESPACE) + accessor = re.search(r'native_instrument_feed_view\s*\([^;]+;', value, re.S) + if accessor is None or '=' in accessor.group(0): + raise ValueError('bar accessor must require its explicit override argument') + for forbidden in ('stage_instrument_feed_views', 'stage_lower_path_source', + 'NativeStrategyHost', 'NativeExecutionConsumer'): + if forbidden in header: + raise ValueError('bar-view staging/query belongs to the private consumer: ' + forbidden) + source = versioned(files[NATIVE_BAR_VIEWS_SOURCE], 'pineforge', NATIVE_BAR_VIEWS_NAMESPACE) + require_namespace_functions(source, ('native_instrument_feed_view', 'native_intrabar_path_digest'), + NATIVE_BAR_VIEWS_NAMESPACE) + for path, expected in BAR_VIEW_READS.items(): + actual = tuple(line.strip() for line in clean(files[path]).splitlines() + if re.search(r'(?:\.|->)(?:bars|close_ms)\b', line)) + if actual != expected: + added = [line for line in actual if line not in expected] + missing = [line for line in expected if line not in actual] + raise ValueError(path + ': bar/close access inventory drift; added=' + + repr(added) + '; missing=' + repr(missing)) + routes = { + 'src/native_execution_consumer.cpp': ( + 'candidate, views', 'bar_views());', 'spec->instrument_feeds[subscription.instrument_feed], slot'), + 'src/source/pine_security_eval.cpp': ( + 'native_instrument_feed_view(installed_symbol_feed(site.feed), slot)', + 'native_instrument_feed_view(feed, nullptr)'), + 'src/source/pine_strategy_host.cpp': ( + 'native_instrument_feed_view(feed, slot)', 'stage_instrument_feed_views(nullptr, 0)'), + 'src/source/pine_adapter.cpp': ('normalize_projected_spec(spec)',), + } + for path, tokens in routes.items(): + for token in tokens: + if token not in clean(files[path]): + raise ValueError(path + ': missing view route: ' + token) + + +def self_test_bar_views(): + files = load(ROOT) + check_native_bar_views(files) + mutations = ( + (NATIVE_BAR_VIEWS_HEADER, 'inline namespace native_bar_views_v1', + 'inline namespace native_bar_views_v2'), + (NATIVE_BAR_VIEWS_HEADER, 'const NativeInstrumentFeedView* borrowed) noexcept;', + 'const NativeInstrumentFeedView* borrowed = nullptr) noexcept;'), + (NATIVE_BAR_VIEWS_HEADER, 'const NativeLowerPathSource* lower_path', + 'NativeLowerPathSource* lower_path'), + ('src/source/pine_adapter.cpp', 'normalize_projected_spec(spec)', + 'validate_native_run_spec(spec)'), + ('src/source/pine_security_eval.cpp', 'native_instrument_feed_view(feed, nullptr)', + 'native_instrument_feed_view(feed, nullptr).bars'), + ) + for path, before, after in mutations: + if before not in files[path]: + raise ValueError('bar-view self-test has a stale mutation: ' + before) + changed = dict(files) + changed[path] = files[path].replace(before, after, 1) + try: + check_native_bar_views(changed) + except ValueError: + continue + raise ValueError('bar-view guard accepted mutation: ' + before) + + def check_texts(files): check_native_fx_curve(files) + check_native_bar_views(files) identity = versioned(files[FILES[11]], "pineforge::native_order", "native_order_v1") require(identity, ("RunIdentity", "RequestHandle", "Birth"), "native_order_v1", r'\b(?:class|struct)\s+NAME\s*\{') @@ -557,7 +831,10 @@ def check_texts(files): 'LegacyTolerant = FeedTolerant,'): if alias not in spec: raise ValueError('native_run_spec_v3 dropped a deprecated alias: ' + alias) - spec_src = versioned(files[FILES[5]], "pineforge", "native_run_spec_v3") + if files[FILES[5]].count(SPEC_VIEWS_BRIDGE) != 1: + raise ValueError('context spec validation/digest must preserve their exact v1 bridge') + spec_src = versioned(files[FILES[5]].replace(SPEC_VIEWS_BRIDGE, '', 1), + "pineforge", "native_run_spec_v3") require_namespace_functions( spec_src, ("validate_native_run_spec", "normalize_native_run_spec", "validate_native_timeframe_subscriptions", @@ -1163,12 +1440,16 @@ def check(root=ROOT): if __name__ == "__main__": + if sys.argv[1:] == ['--self-test-bar-views']: + self_test_bar_views() + print('bar-view ownership, explicit slots and read inventory mutations ... OK') + raise SystemExit(0) if sys.argv[1:] == ['--self-test-tempdir']: self_test_tempdir() print('historical provider extraction stays outside the repository ... OK') raise SystemExit(0) if len(sys.argv) != 1: - raise SystemExit('usage: check_native_cpp_versions.py [--self-test-tempdir]') + raise SystemExit('usage: check_native_cpp_versions.py [--self-test-tempdir | --self-test-bar-views]') absent = missing_private_fixtures() if absent: # R2-F4: the text half reads only public sources, so it runs in the diff --git a/scripts/test_native_cpp_versions.py b/scripts/test_native_cpp_versions.py index 6d2df3048..0b3dcd737 100644 --- a/scripts/test_native_cpp_versions.py +++ b/scripts/test_native_cpp_versions.py @@ -906,7 +906,16 @@ def reject(self, path, before, after): check_texts(changed) def test_new_paths_append_without_reindexing_existing_pins(self): - self.assertEqual(FILES[-2:], (NATIVE_FX_CURVE_HEADER, NATIVE_FX_CURVE_SOURCE)) + self.assertEqual(FILES[12:14], (NATIVE_FX_CURVE_HEADER, NATIVE_FX_CURVE_SOURCE)) + self.assertEqual(FILES[14:], ( + 'include/pineforge/native_bar_views.hpp', + 'src/native_bar_views.cpp', + 'src/source/pine_security_eval.cpp', + 'src/source/pine_strategy_host.cpp', + 'src/source/pine_adapter.cpp', + 'src/source/pine_scheduler_native.cpp', + 'src/source/magnifier_intrabars.cpp', + )) self.assertEqual(FILES[11], 'include/pineforge/native_order_identity.hpp') self.assertEqual(FILES.count(NATIVE_FX_CURVE_HEADER), 1) self.assertEqual(FILES.count(NATIVE_FX_CURVE_SOURCE), 1) diff --git a/src/native_bar_views.cpp b/src/native_bar_views.cpp new file mode 100644 index 000000000..8c1dd9019 --- /dev/null +++ b/src/native_bar_views.cpp @@ -0,0 +1,68 @@ +#include +#include + +#include + +namespace pineforge { +inline namespace native_bar_views_v1 { + +NativeInstrumentFeedReadView native_instrument_feed_view( + const NativeInstrumentFeed& owned, + const NativeInstrumentFeedView* borrowed) noexcept { + if (borrowed != nullptr) + return {borrowed->bars, borrowed->close_ms, borrowed->bars.size}; + return {{owned.bars.data(), owned.bars.size()}, + owned.close_ms.data(), owned.close_ms.size()}; +} + +std::uint64_t native_intrabar_path_digest( + const IntrabarPath& path, const NativeLowerPathSource* source) { + if (source == nullptr) + return native_run_spec_v3::native_intrabar_path_digest(path); + const auto* lower = path.lower(); + if (lower == nullptr || !lower->bars.empty()) + throw coded(RunFailureCode::engine_invariant, {}, + "lower source lost its metadata"); + struct Fold { + std::uint64_t h = UINT64_C(1469598103934665603); + std::size_t seen = 0; + std::size_t count = 0; + void bytes(const void* data, std::size_t n) noexcept { + const auto* at = static_cast(data); + for (std::size_t i = 0; i < n; ++i) { + h ^= at[i]; + h *= UINT64_C(1099511628211); + } + } + void u(std::uint64_t value) noexcept { bytes(&value, sizeof(value)); } + void i(std::int64_t value) noexcept { bytes(&value, sizeof(value)); } + void d(double value) noexcept { bytes(&value, sizeof(value)); } + } fold; + fold.u(path.value.index()); + fold.u(lower->tf.size()); + fold.bytes(lower->tf.data(), lower->tf.size()); + fold.i(lower->samples); + fold.u(static_cast(lower->distribution)); + fold.u(lower->volume_weighted ? 1u : 0u); + fold.i(lower->volume_weighted_min_samples); + fold.i(lower->volume_weighted_max_samples); + fold.u(static_cast(lower->sample_eligibility)); + fold.count = source->count(); + fold.u(fold.count); + const bool complete = source->for_each( + [](void* state, const Bar& bar) noexcept { + auto& f = *static_cast(state); + if (f.seen == f.count) return false; + ++f.seen; + f.d(bar.open); f.d(bar.high); f.d(bar.low); f.d(bar.close); + f.d(bar.volume); f.i(bar.timestamp); + return true; + }, &fold); + if (!complete || fold.seen != fold.count) + throw coded(RunFailureCode::engine_invariant, {}, + "lower source count and walk disagree"); + return fold.h; +} + +} // inline namespace native_bar_views_v1 +} // namespace pineforge diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index e21d17b0e..8212ee702 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -1706,6 +1706,11 @@ bool NativeExecutionConsumer::admit_public_begin(BacktestEngine& engine, const c present_refusal(engine, not_ready_text); return false; } + if (detached_bar_views_) { + present_refusal(engine, "native Ready bar binding was detached", + RunFailureCode::engine_invariant); + return false; + } return true; } @@ -1759,6 +1764,7 @@ bool NativeExecutionConsumer::prepare_public_begin( try { host->prepare_native_begin(args); } catch (const std::exception& e) { + pending_bar_views_ = {}; preparing_begin_ = false; // A provider's begin-time validation is a public-entry refusal. It // has not started a native run or consumed an identity, so preserve a @@ -1767,6 +1773,7 @@ bool NativeExecutionConsumer::prepare_public_begin( render_exception(engine, e); return false; } catch (...) { + pending_bar_views_ = {}; preparing_begin_ = false; render_unclassified(engine, "native pre-begin provider exception"); return false; @@ -1893,6 +1900,48 @@ void NativeExecutionConsumer::refuse_source_mutation(const char* operation) { (operation ? operation : "")); } +void NativeExecutionConsumer::stage_instrument_feed_views( + const NativeInstrumentFeedView* const* views, std::size_t count) { + if (count != 0 && views == nullptr) + throw coded(RunFailureCode::engine_invariant, {}, + "nonempty feed view table is null"); + std::vector> descriptors(count); + std::vector slots(count, nullptr); + for (std::size_t i = 0; i < count; ++i) { + if (views[i] == nullptr) continue; + descriptors[i] = *views[i]; + slots[i] = &*descriptors[i]; + } + pending_bar_views_.descriptors.swap(descriptors); + pending_bar_views_.slots.swap(slots); +} + +void NativeExecutionConsumer::stage_lower_path_source( + std::unique_ptr source) noexcept { + pending_bar_views_.lower_path = std::move(source); +} + +NativeBarViews NativeExecutionConsumer::bar_views() const noexcept { + if (!std::holds_alternative(state_) && !running()) return {}; + return active_bar_views_.context(); +} + +void NativeExecutionConsumer::detach_bar_views() noexcept { + detached_bar_views_ = detached_bar_views_ || active_bar_views_.borrowed(); + // Keep only borrowed row counts in the copied descriptors. Their data + // pointers and table are inaccessible after detachment; owned counts stay + // in the retained spec. Component digests already include these counts. + for (auto& descriptor : active_bar_views_.descriptors) { + if (!descriptor) continue; + descriptor->bars.data = nullptr; + descriptor->close_ms = nullptr; + } + active_bar_views_.slots.clear(); + active_bar_views_.lower_path.reset(); + active_bar_views_.lower_segment = {nullptr, 0}; + pending_bar_views_ = {}; +} + bool NativeExecutionConsumer::stage_account_currency_fx_series( const std::vector& timestamps, const std::vector& rates) { if (timestamps.size() != rates.size()) return false; @@ -1926,13 +1975,14 @@ bool NativeExecutionConsumer::stage_account_currency_fx_series( const NativeExecutionConsumer::SpecBarDigests& NativeExecutionConsumer::spec_bar_digests( const NativeRunSpec& spec) const noexcept { if (!spec_bar_digests_) { - SpecBarDigests digests; - digests.intrabar = native_intrabar_path_digest(spec.intrabar); + SpecBarDigests digests = bound_bar_components_.value_or(SpecBarDigests{}); + if (!bound_bar_components_) + digests.intrabar = native_intrabar_path_digest(spec.intrabar); if (!spec.subscriptions.empty()) digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); if (spec.auxiliary_feed) digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); - if (!spec.instrument_feeds.empty()) + if (!bound_bar_components_ && !spec.instrument_feeds.empty()) digests.instruments = native_instrument_feeds_digest(spec.instrument_feeds); spec_bar_digests_ = digests; } @@ -2647,6 +2697,13 @@ NativeSetupResult NativeExecutionConsumer::configure(BacktestEngine& engine, NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine, const NativeRunSpec* borrowed, NativeRunSpec* owned) { + // Every exit consumes pending, including a phase refusal or a digest + // exception. The active binding is changed only at the Applied commit. + struct DiscardPending { + BarViewBinding& pending; + ~DiscardPending() { pending = {}; } + } discard{pending_bar_views_}; + const NativeBarViews views = pending_bar_views_.context(); NativeSetupResult result; const NativeRunSpec* prior_spec = nullptr; // A refusal for the host's phase judges no field: it is WrongPhase at @@ -2682,7 +2739,7 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine } std::optional copied; NativeRunSpec& candidate = owned != nullptr ? *owned : copied.emplace(*borrowed); - const auto validation = normalize_native_run_spec(candidate); + const auto validation = native_bar_views_v1::normalize_native_run_spec(candidate, views); result.validation = validation; if (!validation) { fail(engine, NativeFailure{NativeFailureCode::InvalidSpecification, @@ -2710,6 +2767,13 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine return result; } } + SpecBarDigests components; + components.intrabar = native_bar_views_v1::native_intrabar_path_digest( + candidate.intrabar, views.lower_path); + if (!candidate.instrument_feeds.empty()) + components.instruments = native_bar_views_v1::native_instrument_feeds_digest( + candidate.instrument_feeds, views); + const std::size_t lower_count = views.lower_path == nullptr ? 0 : views.lower_path->count(); auto parsed_session = native_calendar::parse_session(candidate.session, candidate.timezone); if (!parsed_session) { fail(engine, NativeFailure{NativeFailureCode::Calendar, NativeFailureOperation::Configure}); @@ -2751,6 +2815,10 @@ NativeSetupResult NativeExecutionConsumer::configure_spec(BacktestEngine& engine // persists across a completed/aborted handle and is reapplied by the next // provider begin, so it must survive that provider's configure call. if (!staged_ingress_fx_) staged_fx_curve_.reset(); + active_bar_views_ = std::move(pending_bar_views_); + active_bar_views_.lower_count = lower_count; + bound_bar_components_ = components; + detached_bar_views_ = false; forget_spec_digests(); leave_running(); state_ = NativeReady{std::move(candidate)}; @@ -9465,9 +9533,9 @@ NativeSetupResult NativeExecutionConsumer::declare_timeframe_subscriptions( result.validation = {NativeRunSpecError::WrongPhase, NativeRunSpecField::None}; return result; } - result.validation = validate_native_timeframe_subscriptions( + result.validation = native_bar_views_v1::validate_native_timeframe_subscriptions( declared, running->spec.input_tf, running->spec.timeframe_undetected, - running->spec.auxiliary_feed, running->spec.instrument_feeds); + running->spec.auxiliary_feed, running->spec.instrument_feeds, bar_views()); if (!result.validation) return result; running->spec.subscriptions = std::move(declared); forget_spec_digests(); @@ -9490,9 +9558,9 @@ NativeSetupResult NativeExecutionConsumer::declare_auxiliary_feed( result.validation = {NativeRunSpecError::WrongPhase, NativeRunSpecField::None}; return result; } - result.validation = validate_native_timeframe_subscriptions( + result.validation = native_bar_views_v1::validate_native_timeframe_subscriptions( running->spec.subscriptions, running->spec.input_tf, - running->spec.timeframe_undetected, declared, running->spec.instrument_feeds); + running->spec.timeframe_undetected, declared, running->spec.instrument_feeds, bar_views()); if (!result.validation) return result; running->spec.auxiliary_feed = std::move(declared); forget_spec_digests(); @@ -9938,7 +10006,11 @@ bool NativeExecutionConsumer::pump_instrument_subscription( render(engine, "native instrument series lost its feed"); return false; } - const NativeInstrumentFeed& feed = spec->instrument_feeds[subscription.instrument_feed]; + const auto context = bar_views(); + const auto* slot = context.instrument_feed_count == 0 + ? nullptr : context.instrument_feeds[subscription.instrument_feed]; + const auto feed = native_instrument_feed_view( + spec->instrument_feeds[subscription.instrument_feed], slot); // The input's own calendar interval. A raw label partition presents every // input as a zero-length interval (timestamp_partition), which states no // close to judge a feed bar against, so the rule reads the interval the @@ -9952,29 +10024,29 @@ bool NativeExecutionConsumer::pump_instrument_subscription( horizon = *resolved; } } - const std::size_t total = feed.bars.size(); + const std::size_t total = feed.bars.size; bool delivered = false; while (subscription.instrument_cursor < total) { const std::size_t at = subscription.instrument_cursor; const bool visible = subscription.lookahead - ? feed.bars[at].timestamp <= horizon.open_ms + ? feed.bars.data[at].timestamp <= horizon.open_ms : feed.close_ms[at] <= horizon.last_traded_close_ms; if (!visible) break; ++subscription.instrument_cursor; delivered = true; // The pull accessor answers with this bar for the whole callback. - subscription.latest = feed.bars[at]; + subscription.latest = feed.bars.data[at]; NativeTimeframeBarContext context; context.subscription = subscription.index; - context.interval.open_ms = feed.bars[at].timestamp; - context.interval.eligible_open_ms = feed.bars[at].timestamp; + context.interval.open_ms = feed.bars.data[at].timestamp; + context.interval.eligible_open_ms = feed.bars.data[at].timestamp; context.interval.last_traded_close_ms = feed.close_ms[at]; context.interval.next_period_open_ms = feed.close_ms[at]; context.interval.next_input_open_ms = - at + 1 < total ? feed.bars[at + 1].timestamp : feed.close_ms[at]; + at + 1 < total ? feed.bars.data[at + 1].timestamp : feed.close_ms[at]; context.completion = NativeCompletionKind::Confirmed; context.delivered_at_ms = bar.timestamp; - if (!invoke_timeframe_callback(engine, feed.bars[at], context)) return false; + if (!invoke_timeframe_callback(engine, feed.bars.data[at], context)) return false; } // gaps: the subscription contract's rule, on the pull side. An instrument // series has no evaluator of the kernel's to clear on the push side; its @@ -10517,6 +10589,11 @@ bool NativeExecutionConsumer::stream_begin(BacktestEngine& engine, args.warmup_n = n_warmup; if (!prepare_public_begin(engine, args)) return false; if (!admit_public_begin(engine, "native stream_begin requires Ready")) return false; + if (active_bar_views_.lower_path) { + present_refusal(engine, "native lower source cannot begin a stream", + RunFailureCode::engine_invariant); + return false; + } clear_rendered(engine); engine.last_run_status_ = 0; engine.abort_requested_.store(false, std::memory_order_relaxed); @@ -12735,4 +12812,23 @@ uint64_t native_run_spec_digest(const NativeRunSpec& spec) noexcept { } } // inline namespace native_run_spec_v3 + +inline namespace native_bar_views_v1 { + +std::uint64_t native_run_spec_digest(const NativeRunSpec& spec, const NativeBarViews& views) { + NativeExecutionConsumer::SpecBarDigests digests; + digests.intrabar = native_bar_views_v1::native_intrabar_path_digest(spec.intrabar, views.lower_path); + if (!spec.subscriptions.empty()) + digests.subscriptions = native_timeframe_subscriptions_digest(spec.subscriptions); + if (spec.auxiliary_feed) + digests.auxiliary = native_auxiliary_feed_digest(*spec.auxiliary_feed); + if (!spec.instrument_feeds.empty()) + digests.instruments = native_bar_views_v1::native_instrument_feeds_digest(spec.instrument_feeds, views); + Fnv f; + f.run_base = spec.identity.run_number; + hash_spec(f, spec, &digests); + return f.h; +} + +} // inline namespace native_bar_views_v1 } // namespace pineforge diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 7536b74b9..2de9f1a63 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -3,7 +3,7 @@ #include #include #include - +#include #include "native_calendar_memo.hpp" #include "native_selected_epoch.hpp" #include "runtime_ambient.hpp" @@ -1694,6 +1694,29 @@ class NativeExecutionConsumer final : public IExecutionConsumer { native_calendar::Timeframe script_tf_{}; std::optional intrabar_tf_; native_calendar::TimeframeCompatibility pairing_{}; + // Representation-only bindings: continuation identity folds their logical + // content through bound_bar_components_, never pointer addresses or tags. + struct BarViewBinding { + std::vector> descriptors; + std::vector slots; + std::unique_ptr lower_path; + NativeBarView lower_segment{nullptr, 0}; + std::size_t lower_count = 0; // represented by the lower component digest + NativeBarViews context() const noexcept { + return {slots.empty() ? nullptr : slots.data(), slots.size(), + lower_path.get(), lower_segment}; + } + bool borrowed() const noexcept { + if (lower_path) return true; + for (const auto* slot : slots) if (slot != nullptr) return true; + return false; + } + }; + BarViewBinding pending_bar_views_; + BarViewBinding active_bar_views_; + std::optional bound_bar_components_; + bool detached_bar_views_ = false; + void detach_bar_views() noexcept; std::optional staged_fx_curve_; bool staged_ingress_fx_ = false; bool in_callback_ = false; @@ -2262,13 +2285,14 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // it is destroyed after the AttemptScope and every pump and callback frame // have unwound. The constructor records, before the entry runs anything, // whether the handle is tracked, whether this call is nested under an - // attempt, a begin, a callback, an input or a dispatch (meaningful for a - // tracked handle only), and std::uncaught_exceptions(). `allow_live` lets a + // attempt, a begin, a callback, an input or a dispatch, and (for a tracked + // handle) std::uncaught_exceptions(). `allow_live` lets a // NORMAL return that leaves the run Running keep its generation open: true // for stream_begin and the pushes and the time advance, false for the batch // runs and stream_end. The destructor is noexcept and renders, dispatches, - // allocates and moves a NativeRunSpec never. It does nothing for an untracked - // handle or a nested call. Otherwise it closes an epoch that is STILL + // allocates and moves a NativeRunSpec never. Its selected-epoch half does + // nothing for an untracked handle or nested call; outer run exits always + // detach borrowed bar bindings. Otherwise it closes an epoch that is STILL // Executing or Capturing (close_open_epoch_at_exit) and leaves a Sealed or // Results one alone, so a refused attempt that merely latched NativeFailed // before admission never rewrites an earlier generation. @@ -2286,13 +2310,15 @@ class NativeExecutionConsumer final : public IExecutionConsumer { bool allow_live) noexcept : c_(c), engine_(engine), tracked_(c.selected_epoch_tracked()), uncaught_(tracked_ ? std::uncaught_exceptions() : 0), - nested_(tracked_ && (c.in_attempt_call_ || c.preparing_begin_ || c.in_callback_ - || c.processing_input_ || c.selected_epoch_.dispatching)), + nested_(c.in_attempt_call_ || c.preparing_begin_ || c.in_callback_ + || c.processing_input_ || c.selected_epoch_.dispatching), allow_live_(allow_live) {} ~EntryExitGuard() { - if (!tracked_ || nested_) return; - c_.close_open_epoch_at_exit(engine_, std::uncaught_exceptions() > uncaught_, - allow_live_); + if (nested_) return; + if (tracked_) + c_.close_open_epoch_at_exit(engine_, std::uncaught_exceptions() > uncaught_, + allow_live_); + if (!allow_live_ || !c_.running()) c_.detach_bar_views(); } EntryExitGuard(const EntryExitGuard&) = delete; EntryExitGuard& operator=(const EntryExitGuard&) = delete; @@ -2396,6 +2422,36 @@ class NativeExecutionConsumer final : public IExecutionConsumer { }; std::optional calc_group_; bool calc_slot_at(std::int64_t at_ms, native_calc::Slot& slot) const; + +public: + // Projection judges the host's pending context without publishing it via + // bar_views(), which is active-only. No installed host surface changes. + NativeRunSpecValidation normalize_projected_spec(NativeRunSpec& spec) const noexcept { + return native_bar_views_v1::normalize_native_run_spec(spec, pending_bar_views_.context()); + } + + // CR-150 V1/V2, CORE-TOP rev 6 amendment (10-18 19:24), changes 1-2: + // staging only replaces pending descriptors/source. It never judges the + // lifecycle or changes active state; only an applied configure binds and + // judges pending data against candidate metadata. Refused configure or + // unwound begin preparation discards it. No Ready restaging; all existing + // lifecycle outcomes remain configure's responsibility. + // Copy nullable descriptors/table, never bar/close arrays. Descriptor + // allocation throws std::bad_alloc with the prior pending slot preserved. + // Nonzero count with a null table throws coded engine_invariant. + void stage_instrument_feed_views(const NativeInstrumentFeedView* const* views, + std::size_t count); + // Transfer exclusive ownership to the pending slot; null clears it and + // replacement destroys its old pending source, without touching active + // bindings. Binding checks source metadata through the normalizer. A + // lower-path source bound at stream_begin is engine_invariant. + void stage_lower_path_source(std::unique_ptr source) noexcept; + // Allocation-free query of the active binding in Ready/Running: bound + // descriptors/source and the kernel's current segment. Empty when nothing + // is bound and after detachment; the pending slot is never visible. + // Borrowed pointers expire on replacement/detachment/destruction. A Ready + // binding already detached is engine_invariant at admit_public_begin. + NativeBarViews bar_views() const noexcept; }; inline NativeExecutionConsumer& as_native_consumer(IExecutionConsumer& consumer) { diff --git a/src/native_run_spec.cpp b/src/native_run_spec.cpp index d2b69f877..f4116977d 100644 --- a/src/native_run_spec.cpp +++ b/src/native_run_spec.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -232,7 +233,11 @@ bool valid_instrument_bar(const Bar& bar) noexcept { // The installed instrument feeds, judged without a calendar: each key's // strings and literal, one feed per key, and each feed's bars, closes and // columns. Parsing a literal may allocate; the callers convert that. -Result instrument_feed_shapes(const std::vector& feeds) { +Result instrument_feed_shapes(const std::vector& feeds, + const NativeBarViews& views) { + if (views.instrument_feed_count != 0 + && (views.instrument_feeds == nullptr || views.instrument_feed_count != feeds.size())) + return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; std::vector keys; keys.reserve(feeds.size()); for (std::size_t f = 0; f < feeds.size(); ++f) { @@ -251,18 +256,26 @@ Result instrument_feed_shapes(const std::vector& feeds) { return {Error::DuplicateInstrumentFeed, Field::InstrumentFeedTimeframe}; } keys.push_back(*parsed); - const auto& bars = feed.bars; - for (std::size_t i = 0; i < bars.size(); ++i) { - if (!valid_instrument_bar(bars[i])) + const auto* borrowed = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[f]; + const auto data = native_instrument_feed_view(feed, borrowed); + const auto bars = data.bars; + if ((borrowed != nullptr && !feed.bars.empty()) + || (bars.size != 0 && bars.data == nullptr)) + return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; + for (std::size_t i = 0; i < bars.size; ++i) { + if (!valid_instrument_bar(bars.data[i])) return {Error::InvalidInstrumentFeedBar, Field::InstrumentFeedBars}; - if (i > 0 && bars[i].timestamp <= bars[i - 1].timestamp) + if (i > 0 && bars.data[i].timestamp <= bars.data[i - 1].timestamp) return {Error::UnorderedInstrumentFeedBars, Field::InstrumentFeedBars}; } - if (feed.close_ms.size() != bars.size()) + if ((borrowed != nullptr && !feed.close_ms.empty()) + || data.close_count != bars.size + || (data.close_count != 0 && data.close_ms == nullptr)) return {Error::InvalidInstrumentFeedClose, Field::InstrumentFeedClose}; - for (std::size_t i = 0; i < bars.size(); ++i) { - if (feed.close_ms[i] <= bars[i].timestamp - || (i + 1 < bars.size() && feed.close_ms[i] > bars[i + 1].timestamp)) { + for (std::size_t i = 0; i < bars.size; ++i) { + if (data.close_ms[i] <= bars.data[i].timestamp + || (i + 1 < bars.size && data.close_ms[i] > bars.data[i + 1].timestamp)) { return {Error::InvalidInstrumentFeedClose, Field::InstrumentFeedClose}; } } @@ -273,7 +286,7 @@ Result instrument_feed_shapes(const std::vector& feeds) { !name) { return {Error::InvalidInstrumentFeedColumn, Field::InstrumentFeedColumns}; } - if (column.values.size() != bars.size()) + if (column.values.size() != bars.size) return {Error::InvalidInstrumentFeedColumn, Field::InstrumentFeedColumns}; for (std::size_t other = 0; other < c; ++other) { if (feed.columns[other].name == column.name) @@ -602,7 +615,7 @@ bool valid_legacy_tolerance(NativeFeedTolerance tolerance) noexcept { return (bits & ~kKnown) == 0u; } -Result validate_values(const NativeRunSpec& spec) noexcept { +Result validate_values(const NativeRunSpec& spec, const NativeBarViews& views) noexcept { const bool require_timeframes = !spec.timeframe_undetected; const struct { const std::string& value; @@ -727,7 +740,7 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } Field shapes_field = Field::InstrumentFeedTimeframe; try { - if (const auto feeds = instrument_feed_shapes(spec.instrument_feeds); !feeds) + if (const auto feeds = instrument_feed_shapes(spec.instrument_feeds, views); !feeds) return feeds; shapes_field = Field::SubscriptionTimeframe; if (const auto series = @@ -741,6 +754,9 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } catch (...) { return {Error::CalendarFailure, shapes_field}; } + if (views.lower_path != nullptr + && (spec.intrabar.lower() == nullptr || !spec.intrabar.lower()->bars.empty())) + return {Error::InvalidIntrabarPath, Field::IntrabarTimeframe}; if (spec.intrabar.value.index() > 2) { return {Error::InvalidIntrabarPath, Field::IntrabarTimeframe}; } @@ -782,8 +798,10 @@ Result validate_values(const NativeRunSpec& spec) noexcept { } // namespace -NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noexcept { - const auto values = validate_values(spec); +namespace { +NativeRunSpecValidation validate_run_spec_views(const NativeRunSpec& spec, + const NativeBarViews& views) noexcept { + const auto values = validate_values(spec, views); if (!values) return values; Field active_field = Field::InputTimeframe; @@ -857,6 +875,12 @@ NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noex } } +} // namespace + +NativeRunSpecValidation validate_native_run_spec(const NativeRunSpec& spec) noexcept { + return validate_run_spec_views(spec, {}); +} + NativeRunSpecValidation normalize_native_run_spec(NativeRunSpec& spec) noexcept { const auto result = validate_native_run_spec(spec); if (!result) return result; @@ -995,7 +1019,7 @@ NativeRunSpecValidation validate_native_timeframe_subscriptions( NativeRunSpecValidation validate_native_instrument_feeds( const std::vector& instrument_feeds) noexcept { try { - return instrument_feed_shapes(instrument_feeds); + return instrument_feed_shapes(instrument_feeds, {}); } catch (const std::bad_alloc&) { return {Error::AllocationFailure, Field::InstrumentFeedTimeframe}; } catch (...) { @@ -1003,12 +1027,14 @@ NativeRunSpecValidation validate_native_instrument_feeds( } } -NativeRunSpecValidation validate_native_timeframe_subscriptions( +namespace { +NativeRunSpecValidation validate_subscription_views( const std::vector& subscriptions, const std::string& input_tf, bool timeframe_undetected, const std::optional& auxiliary_feed, - const std::vector& instrument_feeds) noexcept { - const auto instruments = validate_native_instrument_feeds(instrument_feeds); + const std::vector& instrument_feeds, + const NativeBarViews& views) noexcept { + const auto instruments = native_bar_views_v1::validate_native_instrument_feeds(instrument_feeds, views); if (!instruments) return instruments; const auto feed = validate_native_auxiliary_feed(auxiliary_feed, input_tf, timeframe_undetected); @@ -1037,6 +1063,17 @@ NativeRunSpecValidation validate_native_timeframe_subscriptions( } } +} // namespace + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& instrument_feeds) noexcept { + return validate_subscription_views(subscriptions, input_tf, timeframe_undetected, + auxiliary_feed, instrument_feeds, {}); +} + NativeRunSpecValidation validate_native_auxiliary_feed( const std::optional& auxiliary_feed, const std::string& input_tf, bool timeframe_undetected) noexcept { @@ -1121,8 +1158,10 @@ std::uint64_t native_auxiliary_feed_digest(const NativeAuxiliaryFeed& feed) noex return state; } -std::uint64_t native_instrument_feeds_digest( - const std::vector& instrument_feeds) noexcept { +namespace { +std::uint64_t digest_instrument_feed_views( + const std::vector& instrument_feeds, + const NativeBarViews& views) noexcept { std::uint64_t state = 1469598103934665603ULL; const auto bytes = [&state](const void* data, std::size_t count) noexcept { const auto* values = static_cast(data); @@ -1144,15 +1183,19 @@ std::uint64_t native_instrument_feeds_digest( bytes(value.data(), value.size()); }; u(instrument_feeds.size()); - for (const auto& feed : instrument_feeds) { + for (std::size_t at = 0; at < instrument_feeds.size(); ++at) { + const auto& feed = instrument_feeds[at]; + const auto* borrowed = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[at]; + const auto data = native_instrument_feed_view(feed, borrowed); s(feed.instrument); s(feed.tf); - u(feed.bars.size()); - for (std::size_t k = 0; k < feed.bars.size(); ++k) { - const Bar& bar = feed.bars[k]; + u(data.bars.size); + for (std::size_t k = 0; k < data.bars.size; ++k) { + const Bar& bar = data.bars.data[k]; d(bar.open); d(bar.high); d(bar.low); d(bar.close); d(bar.volume); i(bar.timestamp); - i(k < feed.close_ms.size() ? feed.close_ms[k] : 0); + i(k < data.close_count ? data.close_ms[k] : 0); } u(feed.columns.size()); for (const auto& column : feed.columns) { @@ -1164,5 +1207,49 @@ std::uint64_t native_instrument_feeds_digest( return state; } +} // namespace + +std::uint64_t native_instrument_feeds_digest( + const std::vector& instrument_feeds) noexcept { + return digest_instrument_feed_views(instrument_feeds, {}); +} + } // inline namespace native_run_spec_v3 + +inline namespace native_bar_views_v1 { + +NativeRunSpecValidation normalize_native_run_spec( + NativeRunSpec& spec, const NativeBarViews& views) noexcept { + const auto judged = native_run_spec_v3::validate_run_spec_views(spec, views); + if (!judged) return judged; + if (spec.fee_value == 0.0) spec.fee_value = 0.0; + return {}; +} + +NativeRunSpecValidation validate_native_instrument_feeds( + const std::vector& feeds, const NativeBarViews& views) noexcept { + try { + return native_run_spec_v3::instrument_feed_shapes(feeds, views); + } catch (const std::bad_alloc&) { + return {NativeRunSpecError::AllocationFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } catch (...) { + return {NativeRunSpecError::CalendarFailure, NativeRunSpecField::InstrumentFeedTimeframe}; + } +} + +NativeRunSpecValidation validate_native_timeframe_subscriptions( + const std::vector& subscriptions, + const std::string& input_tf, bool timeframe_undetected, + const std::optional& auxiliary_feed, + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::validate_subscription_views( + subscriptions, input_tf, timeframe_undetected, auxiliary_feed, feeds, views); +} + +std::uint64_t native_instrument_feeds_digest( + const std::vector& feeds, const NativeBarViews& views) noexcept { + return native_run_spec_v3::digest_instrument_feed_views(feeds, views); +} + +} // inline namespace native_bar_views_v1 } // namespace pineforge diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index af99883b2..8e7ff61d9 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2994,7 +2994,9 @@ NativeRunSpec PineExecutionAdapter::project(const PineStrategyConfig& config, // (observe_terminal_receipts), so it keeps only the unread window: the // kernel default, which folds nothing into the spec digest. spec.event_retention = NativeEventRetention::Window; - const auto validation = validate_native_run_spec(spec); + const auto validation = host_ != nullptr + ? NativeExecutionConsumer::bound(*host_).normalize_projected_spec(spec) + : native_bar_views_v1::normalize_native_run_spec(spec, {}); if (!validation) { throw coded(run_spec_field_failure(validation.field), "Pine adapter produced invalid native run spec field " diff --git a/src/source/pine_security_eval.cpp b/src/source/pine_security_eval.cpp index 08a930d65..e19acc887 100644 --- a/src/source/pine_security_eval.cpp +++ b/src/source/pine_security_eval.cpp @@ -1091,8 +1091,13 @@ void source::PineStrategyHost::restore_symbol_feeds() { for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& held = spec->instrument_feeds[i]; NativeInstrumentFeed& feed = symbol_feeds_[i]; - feed.bars = held.bars; - feed.close_ms = held.close_ms; + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[i]; + if (slot == nullptr) { + feed.bars = held.bars; + feed.close_ms = held.close_ms; + } for (std::size_t c = 0; c < feed.columns.size(); ++c) feed.columns[c].values = held.columns[c].values; } @@ -1283,13 +1288,16 @@ void source::PineStrategyHost::read_ahead_foreign_security_sites(const Bar& char : is_na(chart_close)) { continue; } - const NativeInstrumentFeed& feed = installed_symbol_feed(site.feed); - while (static_cast(site.delivered) < feed.bars.size()) { + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[site.feed]; + const auto feed = native_instrument_feed_view(installed_symbol_feed(site.feed), slot); + while (static_cast(site.delivered) < feed.bars.size) { const auto at = static_cast(site.delivered); - const bool visible = site.lookahead ? feed.bars[at].timestamp <= chart_bar.timestamp + const bool visible = site.lookahead ? feed.bars.data[at].timestamp <= chart_bar.timestamp : feed.close_ms[at] <= chart_close; if (!visible) break; - run_foreign_site_bar(site, feed.bars[at], feed.close_ms[at]); + run_foreign_site_bar(site, feed.bars.data[at], feed.close_ms[at]); } } } @@ -1417,9 +1425,10 @@ bool source::PineStrategyHost::set_symbol_feed_column(const std::string& key, if (const std::string why = restore_symbol_feeds_reason(); !why.empty()) return refuse("restore_failed", why); NativeInstrumentFeed& feed = symbol_feeds_[static_cast(found)]; - if (n < 0 || static_cast(n) != feed.bars.size() || (n > 0 && values == nullptr)) + const auto data = native_instrument_feed_view(feed, nullptr); + if (n < 0 || static_cast(n) != data.bars.size || (n > 0 && values == nullptr)) return refuse("column_length", "column '" + name + "' has " + std::to_string(n) - + " values for " + std::to_string(feed.bars.size()) + " bars"); + + " values for " + std::to_string(data.bars.size) + " bars"); // The kernel's judgement of the name, here rather than at the next begin: // a feed of the same key without bars carries it (the count is checked // above, and no column value is refused). @@ -1539,7 +1548,7 @@ void source::PineStrategyHost::refresh_symbol_data_digest() noexcept { for (const auto& feed : symbol_feeds_) { f.s(feed.instrument); f.s(feed.tf); - f.u(feed.bars.size()); + f.u(native_instrument_feed_view(feed, nullptr).bars.size); f.u(feed.columns.size()); for (const auto& column : feed.columns) f.s(column.name); } diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 3d5f7cf65..bd14070c0 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -230,8 +230,12 @@ StagedConfiguration source::PineStrategyHost::staged_configuration() const { staged.instrument_feeds.reserve(symbol_feeds_.size()); for (std::size_t i = 0; i < symbol_feeds_.size(); ++i) { const NativeInstrumentFeed& feed = installed_symbol_feed(i); + const auto views = NativeExecutionConsumer::bound(*this).bar_views(); + const auto* slot = views.instrument_feed_count == 0 + ? nullptr : views.instrument_feeds[i]; + const auto data = native_instrument_feed_view(feed, slot); staged.instrument_feeds.push_back( - {feed.instrument, feed.tf, feed.bars.size(), feed.columns.size()}); + {feed.instrument, feed.tf, data.bars.size, feed.columns.size()}); } return staged; } @@ -342,6 +346,7 @@ void source::PineStrategyHost::prepare_native_begin(const NativeBeginArgs& args) std::vector lent = lend_feed_bytes(symbol_feeds_); NativeRunSpec spec = [&] { try { + NativeExecutionConsumer::bound(*this).stage_instrument_feed_views(nullptr, 0); return adapter_.project(effective, staged, args, path_order, &lent); } catch (...) { return_feed_bytes(symbol_feeds_, lent); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d11ecee74..be69219ab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7087,3 +7087,11 @@ pineforge_add_unit_test(test_cr150_contract_headers) target_sources(test_cr150_contract_headers PRIVATE test_cr150_contract_headers_c.c) set_target_properties(test_cr150_contract_headers PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +# CR-150 L15a: append after every existing line-pinned registration. +list(APPEND TEST_SOURCES test_native_bar_views_l15a) +pineforge_add_unit_test(test_native_bar_views_l15a) + +add_test(NAME test_native_bar_views_guard_l15a + COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_native_cpp_versions.py + --self-test-bar-views) diff --git a/tests/test_cr150_contract_headers.cpp b/tests/test_cr150_contract_headers.cpp index f2b077fe0..74f366aa1 100644 --- a/tests/test_cr150_contract_headers.cpp +++ b/tests/test_cr150_contract_headers.cpp @@ -6,14 +6,18 @@ #include #include #include +#include "../src/native_execution_consumer.hpp" #include #include #include +#include +#include #include #include #include #include +#include // DESIGN revision 6, section 6:1283-1306: fixed prefix, three native // pointers, and the explicitly padded 144-byte receipt. The descriptor's @@ -113,6 +117,67 @@ static_assert(std::is_standard_layout_v); static_assert(std::is_same_v); static_assert(std::is_same_v); +// CORE-TOP rev 6 amendment (10-18 19:24), V1/V2 as amended by changes 1-9. +namespace bv = pineforge::native_bar_views_v1; +using ReadView = bv::NativeInstrumentFeedReadView; +using Views = bv::NativeBarViews; +using Source = bv::NativeLowerPathSource; +using Consumer = pineforge::NativeExecutionConsumer; +static_assert(std::is_standard_layout_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_standard_layout_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(Views{}.instrument_feeds == nullptr); +static_assert(Views{}.instrument_feed_count == 0); +static_assert(Views{}.lower_path == nullptr); +static_assert(Views{}.lower_segment.data == nullptr); +static_assert(Views{}.lower_segment.size == 0); + +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v&, const Views&) noexcept>); +static_assert(std::is_same_v&, const std::string&, bool, + const std::optional&, + const std::vector&, const Views&) noexcept>); +static_assert(std::is_same_v&, + const Views&) noexcept>); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +static_assert(std::is_abstract_v); +static_assert(std::has_virtual_destructor_v); +static_assert(std::is_nothrow_destructible_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +static_assert(std::is_same_v); +static_assert(std::is_same_v) noexcept>); +static_assert(std::is_same_v); + int main() { // CTest reports the build-time witness; it does not exercise future APIs. std::puts("test_cr150_contract_headers: C++ and C contract assertions compiled"); diff --git a/tests/test_native_bar_views_l15a.cpp b/tests/test_native_bar_views_l15a.cpp new file mode 100644 index 000000000..b336afa23 --- /dev/null +++ b/tests/test_native_bar_views_l15a.cpp @@ -0,0 +1,306 @@ +// CR-150 L15a: execute the frozen V1/V2 contract over synthetic data. +// Red on ee3083d4a: the accessor, context overloads and staging members are +// declared but undefined. No compiler or runtime result is claimed locally. +#include "native_instrument_feed_fixture.hpp" +#include "../src/native_execution_consumer.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +using namespace instrument_fixture; +namespace views = pineforge::native_bar_views_v1; + +NativeInstrumentFeed tape() { + FeedBuilder builder("SYN:VIEW", "1", 101.0); + for (int i = 0; i < 6; ++i) + builder.add(monday() + i * kMinute, monday() + (i + 1) * kMinute); + builder.feed.columns.push_back({"x", {1, 2, 3, 4, 5, 6}}); + return builder.feed; +} + +NativeInstrumentFeed metadata(const NativeInstrumentFeed& feed) { + NativeInstrumentFeed result = feed; + result.bars.clear(); + result.close_ms.clear(); + return result; +} + +NativeInstrumentFeedView borrowed(const NativeInstrumentFeed& feed) { + return {{feed.bars.data(), feed.bars.size()}, feed.close_ms.data()}; +} + +NativeRunSpec spec_for(const NativeInstrumentFeed& feed) { + auto spec = continuous_spec("l15a-views", "1"); + spec.instrument_feeds.push_back(feed); + spec.subscriptions.push_back(instrument_series(feed)); + return spec; +} + +void effective_data_and_refusals() { + const auto owned = tape(); + const auto lent = metadata(owned); + const auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + const NativeBarViews context{slots, 1}; + const auto read = native_instrument_feed_view(lent, &descriptor); + assert(read.bars.data == owned.bars.data()); + assert(read.close_ms == owned.close_ms.data()); + assert(read.bars.size == 6 && read.close_count == 6); + const auto fallback = native_instrument_feed_view(owned, nullptr); + assert(fallback.bars.data == owned.bars.data()); + + const NativeInstrumentFeedView empty{{nullptr, 0}, nullptr}; + assert(native_instrument_feed_view(owned, &empty).bars.size == 0); + auto missing_close = owned; + missing_close.close_ms.pop_back(); + assert(native_instrument_feed_view(missing_close, nullptr).close_count == 5); + assert(views::native_instrument_feeds_digest({missing_close}, {}) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest({missing_close})); + + auto logical = spec_for(lent); + const auto reference = spec_for(owned); + assert(views::normalize_native_run_spec(logical, context)); + assert(views::native_instrument_feeds_digest(logical.instrument_feeds, context) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest(reference.instrument_feeds)); + assert(views::native_run_spec_digest(logical, context) + == pineforge::native_run_spec_v3::native_run_spec_digest(reference)); + assert(views::validate_native_timeframe_subscriptions( + logical.subscriptions, logical.input_tf, false, logical.auxiliary_feed, + logical.instrument_feeds, context)); + + auto bad_column = logical; + bad_column.instrument_feeds[0].columns[0].values.pop_back(); + const auto column_error = views::normalize_native_run_spec(bad_column, context); + assert(column_error.error == NativeRunSpecError::InvalidInstrumentFeedColumn); + assert(column_error.field == NativeRunSpecField::InstrumentFeedColumns); + + auto bad_close = owned.close_ms; + bad_close[0] = owned.bars[0].timestamp; + const NativeInstrumentFeedView invalid{{owned.bars.data(), owned.bars.size()}, bad_close.data()}; + const NativeInstrumentFeedView* bad_slots[] = {&invalid}; + const auto close_error = views::validate_native_instrument_feeds({lent}, {bad_slots, 1}); + assert(close_error.error == NativeRunSpecError::InvalidInstrumentFeedClose); + assert(close_error.field == NativeRunSpecField::InstrumentFeedClose); + const auto conflict = views::validate_native_instrument_feeds({owned}, context); + assert(conflict.error == NativeRunSpecError::InvalidInstrumentFeedBar); + const auto mismatch = views::validate_native_instrument_feeds({}, context); + assert(mismatch.error == NativeRunSpecError::InvalidInstrumentFeedBar); + + auto empty_meta = lent; + empty_meta.columns.clear(); + const NativeInstrumentFeedView* empty_slots[] = {&empty}; + assert(views::validate_native_instrument_feeds({empty_meta}, {empty_slots, 1})); + const NativeInstrumentFeedView* mixed_slots[] = {nullptr, &descriptor}; + auto other = owned; + other.instrument = "SYN:OWNED"; + assert(views::validate_native_instrument_feeds({other, lent}, {mixed_slots, 2})); + + auto nan_twin = owned; + std::uint64_t bits = UINT64_C(0x7ff8000000000042); + std::memcpy(&nan_twin.bars[0].volume, &bits, sizeof(bits)); + const auto nan_descriptor = borrowed(nan_twin); + const NativeInstrumentFeedView* nan_slots[] = {&nan_descriptor}; + assert(views::native_instrument_feeds_digest({lent}, {nan_slots, 1}) + == pineforge::native_run_spec_v3::native_instrument_feeds_digest({owned})); +} + +class HashHost : public InstrumentHost { +public: + std::vector hashes; + void on_native_bar(const Bar& bar, const NativeDecisionContext& context) override { + InstrumentHost::on_native_bar(bar, context); + hashes.push_back(native_continuation_hash()); + } +}; + +void binding_delivery_and_detachment() { + auto owned = tape(); + const auto reference = spec_for(owned); + auto logical = spec_for(metadata(owned)); + HashHost copied; + HashHost lent; + auto& consumer = NativeExecutionConsumer::bound(lent); + { + auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + consumer.stage_instrument_feed_views(slots, 1); + } // staging must own descriptor copies, not these stack objects. + assert(consumer.bar_views().instrument_feed_count == 0); + assert(copied.configure_native(reference).status == NativeSetupStatus::Applied); + assert(lent.configure_native(logical).status == NativeSetupStatus::Applied); + const auto active = consumer.bar_views(); + assert(active.instrument_feed_count == 1); + assert(active.instrument_feeds[0]->bars.data == owned.bars.data()); + + // Staging while Ready affects pending only; no Ready restaging. + const NativeInstrumentFeedView empty{{nullptr, 0}, nullptr}; + const NativeInstrumentFeedView* next[] = {&empty}; + consumer.stage_instrument_feed_views(next, 1); + assert(consumer.bar_views().instrument_feeds[0]->bars.size == owned.bars.size()); + consumer.stage_instrument_feed_views(nullptr, 0); + + const auto chart = continuous_quarter_chart(monday(), 1); + // The chart is a valid single 1-minute input regardless of fixture spacing. + copied.run(chart.data(), 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + lent.run(chart.data(), 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(copied.last_error().empty() && lent.last_error().empty()); + assert(copied.deliveries.size() == lent.deliveries.size()); + assert(!lent.deliveries.empty()); + for (std::size_t i = 0; i < copied.deliveries.size(); ++i) { + assert(bars_equal(copied.deliveries[i].bar, lent.deliveries[i].bar)); + assert(copied.deliveries[i].context.interval.last_traded_close_ms + == lent.deliveries[i].context.interval.last_traded_close_ms); + } + assert(copied.hashes == lent.hashes); + assert(copied.native_continuation_hash() == lent.native_continuation_hash()); + assert(consumer.bar_views().instrument_feed_count == 0); + const auto after = lent.native_continuation_hash(); + owned.bars.clear(); + owned.bars.shrink_to_fit(); + owned.close_ms.clear(); + owned.close_ms.shrink_to_fit(); + assert(lent.native_continuation_hash() == after); +} + +class VectorSource final : public NativeLowerPathSource { + const std::vector& bars_; + int& destroyed_; + bool throws_; + bool selected_ = false; + std::int64_t begin_ = 0; + std::int64_t end_ = 0; + NativeBarView active_{nullptr, 0}; + const Bar* next_ = nullptr; +public: + VectorSource(const std::vector& bars, int& destroyed, bool throws = false) + : bars_(bars), destroyed_(destroyed), throws_(throws) {} + ~VectorSource() noexcept override { ++destroyed_; } + std::size_t count() const override { return bars_.size(); } + bool for_each(Visitor visitor, void* context) const override { + if (throws_) throw std::runtime_error("l15a-walk-fault"); + if (visitor == nullptr) + throw coded(RunFailureCode::engine_invariant, {}, "null visitor"); + for (const auto& bar : bars_) if (!visitor(context, bar)) return false; + return true; + } + NativeBarView segment(std::int64_t begin, std::int64_t end) override { + if (end < begin || (selected_ && (begin < begin_ || end < end_))) + throw coded(RunFailureCode::engine_invariant, {}, "window order"); + if (selected_ && begin == begin_ && end == end_) return active_; + const auto less = [](const Bar& bar, std::int64_t time) { return bar.timestamp < time; }; + const auto first = std::lower_bound(bars_.begin(), bars_.end(), begin, less); + const auto last = std::lower_bound(first, bars_.end(), end, less); + active_ = {first == last ? nullptr : &*first, static_cast(last - first)}; + next_ = last == bars_.end() ? nullptr : &*last; + selected_ = true; + begin_ = begin; + end_ = end; + return active_; + } + const Bar* next_after_segment() const noexcept override { return next_; } +}; + +void lower_context_and_exception_atomicity() { + const auto chart = continuous_quarter_chart(monday(), 2); + auto spec = continuous_spec("l15a-lower", "15"); + IntrabarPath::lower_tf path; + path.tf = "1"; + path.bars = chart; + spec.intrabar.value = path; + auto lent = spec; + lent.intrabar = IntrabarPath{}; + path.bars.clear(); + lent.intrabar.value = path; + + int destroyed = 0; + { + VectorSource source(chart, destroyed); + assert(views::native_intrabar_path_digest(lent.intrabar, &source) + == pineforge::native_run_spec_v3::native_intrabar_path_digest(spec.intrabar)); + const NativeBarViews context{nullptr, 0, &source}; + assert(views::native_run_spec_digest(lent, context) + == pineforge::native_run_spec_v3::native_run_spec_digest(spec)); + } + assert(destroyed == 1); + HashHost host; + auto& consumer = NativeExecutionConsumer::bound(host); + consumer.stage_lower_path_source(std::make_unique(chart, destroyed, true)); + assert(consumer.bar_views().lower_path == nullptr); + bool threw = false; + try { + (void)host.configure_native(lent); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()) == "l15a-walk-fault"; + } + assert(threw); + assert(host.native_state().kind == NativeLifecycleKind::Unconfigured); + assert(consumer.bar_views().lower_path == nullptr); + assert(destroyed == 2); + assert(host.configure_native(spec).status == NativeSetupStatus::Applied); + + HashHost stream; + auto& stream_consumer = NativeExecutionConsumer::bound(stream); + stream_consumer.stage_lower_path_source(std::make_unique(chart, destroyed)); + assert(stream.configure_native(lent).status == NativeSetupStatus::Applied); + assert(stream_consumer.bar_views().lower_path != nullptr); + assert(!stream.stream_begin(chart.data(), static_cast(chart.size()), "15", "15")); + assert(stream_consumer.bar_views().lower_path == nullptr); + assert(destroyed == 3); + (void)stream.native_continuation_hash(); + + // A null staging table is a programmer invariant; active state is intact. + bool invalid_table = false; + try { consumer.stage_instrument_feed_views(nullptr, 1); } + catch (const std::logic_error&) { invalid_table = true; } + assert(invalid_table); + assert(host.native_state().kind == NativeLifecycleKind::Ready); +} + +void refused_configure_discards_pending() { + HashHost host; + auto& consumer = NativeExecutionConsumer::bound(host); + const auto owned = tape(); + auto descriptor = borrowed(owned); + const NativeInstrumentFeedView* slots[] = {&descriptor}; + consumer.stage_instrument_feed_views(slots, 1); + auto bad = spec_for(metadata(owned)); + bad.instrument_feeds[0].columns[0].values.pop_back(); + const auto refused = host.configure_native(bad); + assert(refused.status != NativeSetupStatus::Applied); + assert(consumer.bar_views().instrument_feed_count == 0); + + HashHost retry; + auto& retry_consumer = NativeExecutionConsumer::bound(retry); + const auto logical = spec_for(metadata(owned)); + retry_consumer.stage_instrument_feed_views(slots, 1); + bool table_error = false; + try { retry_consumer.stage_instrument_feed_views(nullptr, 1); } + catch (const std::logic_error&) { table_error = true; } + assert(table_error); + assert(retry.configure_native(logical).status == NativeSetupStatus::Applied); + auto malformed = make_bar(monday(), 0, 1.0); + malformed.high = malformed.low - 1.0; + retry.run(&malformed, 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(retry.native_state().kind == NativeLifecycleKind::Ready); + assert(retry_consumer.bar_views().instrument_feed_count == 0); + const auto valid = make_bar(monday(), 0, 1.0); + retry.run(&valid, 1, "1", "1", false, 4, MagnifierDistribution::ENDPOINTS); + assert(!retry.last_error().empty()); // detached Ready is engine_invariant. +} +} // namespace + +int main() { + effective_data_and_refusals(); + binding_delivery_and_detachment(); + lower_context_and_exception_atomicity(); + refused_configure_discards_pending(); + std::puts("test_native_bar_views_l15a: contract, identity and lifetime assertions passed"); +}