diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 7110412c6..4c27390d3 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -213,8 +213,8 @@ 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` + `validate_precommit` (`pine_adapter.cpp:16506`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:16710`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:550`) / `on_native_applied` (`pine_strategy_host.cpp:622`) 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 @@ -226,10 +226,10 @@ kernel and own these quirks, each at its site: lowering moved the excursion cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther (the private fixture set `e19_allin_trim` holds the tape of the all-in residual's own entry-bar call). `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2817-2818`); + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2822-2823`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:21`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`PendingSameBarCommand` pine_adapter.hpp:1881, `pending_bracket_legs_` pine_adapter.hpp:2515-2533); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1881, `pending_bracket_legs_` pine_adapter.hpp:2522-2548); the retained parent-before-child ordering of live handles — an exactly-shaped entry and its `from_entry` exit, re-created after a named cancel, on a flat book under `process_orders_on_close` (`src/compat/pine/order_priority.cpp:13-59`, applied by `update_l4c_priority` `pine_adapter.cpp:1364-1434`). Kernel priority is acceptance/incarnation order. @@ -237,9 +237,9 @@ 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:12635-12640`); 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:14979`). - **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 @@ -256,12 +256,12 @@ kernel and own these quirks, each at its site: restore's second floor) fail tape-pinned rows (`test_pine_dust_sweep_paired`, `test_tv_money_band_l4b`); `quantity_tolerance` is not their rule (its row below). - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2580-2610`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2595-2625`); the POOC reservation-growth population predicate (`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:17965`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:18034`) 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:24302`) 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,9 +333,9 @@ 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:10599`), 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:6883`). - **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_` @@ -394,7 +394,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files the lines 73-180 earlier drafts cited are and were live code (`ClosedLotExcursionFacts`, `PyramidEntry`, `Trade`, each with its own doc). The arithmetic is adapter-side (`source_money_round` `pine_adapter.cpp:558-576`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2580-2610`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2595-2625`) and only names survive in comments. The TradingView margin-call toggle remains source-owned as `source::PineStrategyHost::set_margin_call_enabled` (`pine_strategy_host.hpp:511`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -710,12 +710,12 @@ 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` | -| `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` | -| `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` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:17616`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:20993`), `chart_day_key` (`pine_adapter.cpp:17513`), `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:16625-16626`): 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:2651-2653`) | 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:2890-2898`, `margin.maintenance_long` `pine_adapter.cpp:2905`) | 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:2860) | 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:2830-2832`) | 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` | | `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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 2442b75c8..6ce98c57d 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,20 +47,20 @@ 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:3439; `entry()` pine_adapter.cpp:7294, `order()` pine_adapter.cpp:14745) | **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:10676 → `Flatten{}` pine_adapter.cpp:10710; `Reduce{ExplicitUnits}` pine_adapter.cpp:9549; `ReverseTo` pine_adapter.cpp:7970) | **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:4417 → `host.replace` pine_adapter.cpp:4842) | **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:14639, `cancel_all` pine_adapter.cpp:14712; `live_by_source_key_` pine_adapter.hpp:2536; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4855) | **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:5061; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:9295) | **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:6716, `cohort_for` pine_adapter.cpp:3015; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:9390) | **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:3448) | **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. | -| 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 | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:11169; lowering `submit_one` pine_adapter.cpp:12047, `BindCohort` pine_adapter.cpp:12510, `submit_leg` pine_adapter.cpp:11803; 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:11429; `materialize_relative_exits` pine_adapter.cpp:14118, pine_adapter.hpp:2266-2278) | **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:27207) | **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:2522-2549); 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:4248, `materialize_snapshot` pine_adapter.cpp:14092), 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:21752, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:21986) | 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:2202). 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 @@ -68,64 +68,64 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | 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 | — | | 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:15136) | **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:15053) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4810) | **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:11291, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:15053, retained best `retained_trail_best` pine_adapter.hpp:353-355, operand / restart rules `source_trail_points` pine_adapter.cpp:11288, `trail_one_shot` pine_adapter.cpp:12634 | 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:7329, pine_adapter.cpp:7329) | **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:13861-15089) | **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:3204) | **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:3075) | **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:3223, `typed_entry_units` pine_adapter.cpp:3241; `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:3196; `percent_commission_live_equity` pine_adapter.cpp:5215) | **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:3030, `PineSizingSnapshot` pine_adapter.hpp:157-169; fill-time exceptions `default_sized` pine_adapter.cpp:8601) | **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:5335, `compute_exit_reservation` pine_adapter.cpp:5375) | **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:5375, `reserved_exit_qty` pine_adapter.cpp:11698) | **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:5357) | 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:8644), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:10168), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:16347), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:16878, pine_adapter.cpp:16931) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:21800) | 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:3204), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:3223, `typed_entry_units` pine_adapter.cpp:3241), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:26794) | 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:9295; ledger `close_batch_bar_` pine_adapter.hpp:2610; 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:2864) | **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:16626) 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:7834, cap left unset `max_open_lots` pine_adapter.cpp:2863-2864) 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:11722). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:17439 and leaves the kernel gate unset pine_adapter.cpp:17458) | **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:17692, `margin_pct` pine_adapter.cpp:17194) | **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:18079, `submit_margin_call_units` pine_adapter.cpp:18112, `schedule_margin_call_path` pine_adapter.cpp:20359) | **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:17736) | **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:19266; 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:17736; 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:19308, `follow_margin_call` pine_adapter.cpp:19357; 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:5802, `apply_fx_opening_margin_slice` pine_adapter.cpp:5814) | **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:17616) | **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:16619) | **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:17649, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:20993) | **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:21036) | **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:17513) | **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:17681, 4× shortfall `4.0 * minimum` pine_adapter.cpp:17770, POOC chronology exception `resting_order` pine_adapter.cpp:20379 (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:18675, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:18675) | 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:4600, `record_market_review` pine_adapter.cpp:21292) | 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 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2821) | **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:22387) | **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. | | 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. | +| 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:6813, `flush_coof_tail` pine_adapter.cpp:7237, `end_coof_recalc` pine_adapter.cpp:6870) | **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). | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2939) | **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 | +| CT11 | *(quirk)* COOF TV specifics: waypoint-only refill (`coof_next_waypoint` pine_adapter.cpp:7086), 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 @@ -143,13 +143,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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 | +| 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:15303) 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:15303, `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. | | 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 | +| 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:14497), `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:17905 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:14979) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -171,7 +171,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | OT1 | Key / value run parameters (`set_input` engine.hpp:1871, refused in-run by `guard_native_mutation` engine.hpp:1876) | **partial** — staging allowed pre-run, getters protected | K (low) | — | F:J1 | (single-source) Nothing for v1: native hosts take constructor parameters. | -| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2737, `chart_timezone` pine_adapter.cpp:2780) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | +| OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2742, `chart_timezone` pine_adapter.cpp:2785) | **yes** — spec fields `ticker` native_run_spec.hpp:636-646; `parse_session` native_calendar.hpp:180-467; intervals on every decision context `input_interval` market_driver.hpp:123-124 | K | — | F:J2 O:G-5 | — | | OT3 | Indicators, series, math, matrix, map, string utils | **yes** — `ta.hpp` includes only `na/series/window_sum` `window_sum` ta.hpp:2-8 | K | L11 (naming only) | F:J3 | (single-source row) TV-calibrated numerics → §2.ii b | | 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) | @@ -204,13 +204,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---| | a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp: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 | | 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: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:2811. 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) | | 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:27207 | 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) | @@ -326,7 +326,7 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 4 | HTF "comes from the kernel feed `set_native_security_feed` / `prepare_native_security_feeds`" (ADR L84-89) | Not usable by a bare host: a partial boundary, not an absent kernel. `prepare_native_security_feeds` is protected with one caller; `configure_security_evaluators` has one caller; the public setter is inert pre-run and refused in-run | `prepare_native_security_feeds` engine.hpp:1639, `configure_security_evaluators` engine.hpp:1322, `set_native_security_feed` engine.hpp:1731, `guard_native_mutation` engine_aux_security.cpp:83 | F O S | | 5 | "`engine_aux_security.cpp` includes `source/pine_strategy_host.hpp`" (ADR L129-132) | False at `73817c1`: its includes are `engine_internal.hpp`, `ta.hpp` and std headers (*fresh* re-read; the run-failure codes added `run_failure.hpp`, a kernel header). No kernel TU includes a source header | `sha256:560a7f68b243d921382a69ed0390c398f28370ac066b84f30b5f96cbb385fb60` engine_aux_security.cpp:6-14 | F | | 6 | "Kernel TUs reference adapter types (`source::StrategyOverrides` …)" (ADR L129-131) | True only as a forward-declared opaque pointer the kernel never dereferences; the independence checker whitelists exactly that symbol | at `73817c1`; overtaken by `3f4fff5b` (§2.ii a), after which the rich begin carries `overrides_opaque` native_host.hpp:758 and the independence checker whitelists nothing | F O | -| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2580; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | +| 7 | `engine.hpp` hosts "the ten-significant-digit money rule, KI-62, the `strategy.close` batching + entry-id ledger, a TradingView margin-call toggle" (ADR L131-134) | Two of four survive as code. Money rule: a comment block; the arithmetic is in the adapter. KI-62: orphaned comments + one live, hashed lot flag. Close batching + id ledger: gone from the kernel (the ledger is in the adapter; the old names survive only in comments). Live: `margin_call_enabled_`, the KI-62 flag and — unmentioned by the ADR — string-sentinel decoding of adapter comments | the money-rule block was lines 927-961 of `engine.hpp` and R5 lane E6 deleted it (lines 73-180 were and are live code), the arithmetic `source_money_round` pine_adapter.cpp:558 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:74, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2595; `closed_trade_close_cause` engine_trade_accessors.cpp:158. **Overtaken:** the margin-call toggle and the string decoding are both gone — see ADR-0001, "Current state vs. target" | F O | | 8 | "No kernel-level C API. `c_abi.cpp` exports only version/descriptor symbols" (ADR L136-138) | 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 | @@ -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:2529-2796) 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. - **(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. @@ -410,7 +410,7 @@ closure markers and `scripts/check_design_inventory.py`). |---|---|---|---| | L0 | — | docs only | — | | L1 | option `PINEFORGE_BUILD_SOURCE_LAYER`, targets `pineforge_kernel` + unchanged `pineforge` (§2.i) | same TUs, same flags; ctest + corpus | kernel-only link of the two examples + the 20 source-free native tests; installed-header closure | -| 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) | +| 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:2860, 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) | @@ -523,12 +523,12 @@ line numbers are re-anchored to this tree (the measurement ran on 683a82f); kern | 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.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2864); 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:16626; 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:17616, `update_risk_state` pine_adapter.cpp:17616): 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:16618), 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:25867); 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:22627); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:20993), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:21025, `submit_intraday_loss_close` pine_adapter.cpp:20993), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:24815); 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:17611, `:17611`, `:17611`, `:17611`, `:17611`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:14712) | `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:17607); 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:17513, `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, @@ -1086,7 +1086,7 @@ row C). row, every placement snapshot the run had accepted (a sorted walk of every key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:330) — one per accepted request, cleared only by -`reset_for_run` pine_adapter.cpp:2396 — and reflected the whole admission +`reset_for_run` pine_adapter.cpp:2398 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` pine_state_hash.cpp:759). `PineScheduler::hash_state` pine_state_hash.cpp:797 re-folded the consumed source prefix too (`consumed` @@ -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:11558). 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 + `revive_brackets_after_margin` pine_adapter.cpp:2202, + `preserved_by_close_all` pine_adapter.cpp:14780), 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:3644). 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:4096); 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 @@ -1600,10 +1600,10 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folded every field unconditionally (native_execution_consumer.cpp:234-256); every block added since folds only when set | Any new field, even defaulted, changes every continuation / broker-state hash, adapter runs included | Fold new blocks only when set, as a tagged extension (precedent `precommit_digest_` native_execution_consumer.hpp: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 | +| 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:18079); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:20359 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:3187, the COOF `at_fill` exception pine_adapter.cpp:8528) | 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:7086) | 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:22387) | 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 | | 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 | @@ -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:881 → `set_risk_direction` pine_adapter.cpp:26609 (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) @@ -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:5802) 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:3260) 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. | @@ -1673,7 +1673,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ``` - `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). -- `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`. +- `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:2860), 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}`. @@ -1726,7 +1726,7 @@ 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:20346). 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). - 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). @@ -1836,7 +1836,7 @@ std::optional risk; // NativeRunSpec - Breach: `MatchRejectReason::RiskLimit` for openings (O; F: `RiskHalted`), an optional kernel-issued flatten through A.3's kernel-originated request (`origin = KernelRisk`, F), and a `NativeRiskEvent` in `native_events` (native_host.hpp:1335) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`session_day_ordinal` native_calendar.hpp:416), not a chart clock. - S's `directions` / `max_units` / `max_position_units` are existing spec fields (MG1-MG3), not duplicated. -- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:17310-17318); a toolkit helper is an acceptable stand-in until L9. +- 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:17445-17453); 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. ### A.9 Single-source proposals not adopted diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index fd9c33a3f..a9426e8a5 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:6585`), 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:17486`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:22387`) reads `chart_day_key` (`src/source/pine_adapter.cpp:22624`) 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:17470`) | 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 | @@ -199,7 +199,7 @@ unless the retention is Full, and `presize_logs` (`src/native_execution_consumer.cpp:10278-10289`) 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 +`NativeEventRetention::Window` (`src/source/pine_adapter.cpp:3001`). K4 still sizes Full runs (driver log and journal) and Commands runs (journal), and its witness, `tests/test_native_batch_log_presize.cpp`, runs under Full. PERF-K24's replay figure (14.9 % less CPU on Linux aarch64) describes the tree before V19-B. In @@ -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:6585`) 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` @@ -241,7 +241,7 @@ Mac's 489 and 412 and 1003 of 1249, and no Mac output moved. - **PERF-L4's retired reader slots.** `event_high_water_reader_` and `terminal_receipt_high_water_reader_` - (`include/pineforge/source/pine_adapter.hpp:2388-2389`), with their type + (`include/pineforge/source/pine_adapter.hpp:2395-2396`), with their type `ReceiptHighWaterReader`, are storage nothing reads or writes since PERF-L4: they keep `PineExecutionAdapter`'s layout, which is part of `PineStrategyHost`'s and so of the generated script's ABI. They go at the next diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 7d3aa9d09..89dd0736a 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -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:17965-17998`), 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 diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index f51067868..1928f05ab 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -2010,6 +2010,13 @@ class PineExecutionAdapter { NativeStrategyHost& require_host() const; bool modeled_input() const; native_order::CohortHandle cohort_for(const SourceId& id); + // held_cohort_ids_ upkeep: hold_cohort after `id`'s cohort gains an + // opening or live units, forget_emptied_cohorts after any cohort loses + // its last of both. + void hold_cohort(const SourceId& id); + void forget_emptied_cohorts(); + const std::vector& held_cohort_ids() const noexcept; + bool held_cohort_ids_exact() const noexcept; std::optional submit_or_replace( native_order::Request request, PlacementSnapshot snapshot, bool opening, const SourceId& replacement_key = {}); @@ -2517,6 +2524,14 @@ class PineExecutionAdapter { std::uint32_t entry_attempts_on_bar_ = 0; std::unordered_map cohorts_by_id_; std::vector cohort_order_; + // The ids whose cohort holds an opening or live units, in cohort_order_ + // order, which is the order of their kernel handles: cohort_for opens the + // handle as it appends the id, and the core numbers handles upward. + // cohorts_by_id_ keeps a cohort for every id the run has used, so a walk + // that reads only `opened` and `live_units_by_origin` walks this instead + // and costs what is open, not every id used so far. Not state: a function + // of cohorts_by_id_ and cohort_order_. + std::vector held_cohort_ids_; PlacementTable placement_; std::unordered_map live_by_source_key_; std::unordered_map bracket_families_; @@ -2845,6 +2860,14 @@ class PineExecutionAdapter { std::vector> released_origins; native_order::RequestHandle lot_probe{}; bool roster_pass_due = true; + // A memo too (a run begin empties it): the kernel rosters that hold + // members, by handle, as of the last pass, and how many of the core's + // cohort receipts it has read. Only an applied add puts a member in a + // roster, so a pass reads these (held_rosters, its working list), not + // every roster the run opened. + std::vector rostered; + std::size_t cohort_receipts_read = 0; + std::vector held_rosters; void clear() noexcept { roots.clear(); askable_origins.clear(); diff --git a/scripts/broker_state_hash_waivers.txt b/scripts/broker_state_hash_waivers.txt index 25f82e901..c7d77c22e 100644 --- a/scripts/broker_state_hash_waivers.txt +++ b/scripts/broker_state_hash_waivers.txt @@ -53,7 +53,7 @@ event_high_water_reader_ # Retired storage (R5 lane PERF-L4): nothing reads or w terminal_receipt_high_water_reader_ # Retired storage (R5 lane PERF-L4): nothing reads or writes it; kept only for PineStrategyHost's layout (script ABI) until the next engine_script_run epoch deletes it. source_pending_view_cache_ # Read-only test projection cache rebuilt from adapter state and never consumed by execution. bar_magnifier_ # install-time fact mirrored from the scheduler (retained_.bar_magnifier is already folded); not evolving broker state (L8h A41(5)) -retired_row_scratch_ # Per-sweep working sets of erase_retired_rows (R5 lane V19-E), emptied at the start of every sweep, and one memo that survives sweeps (R5 lane V19-D: the high water each held leg was read through by the superseded test, emptied at every run begin), which answers exactly what reading every row answers; no value reaches a hash point. +retired_row_scratch_ # Per-sweep working sets of erase_retired_rows (R5 lane V19-E), emptied at the start of every sweep, and one memo that survives sweeps (R5 lane V19-D: the high water each held leg was read through by the superseded test, emptied at every run begin), which answers exactly what reading every row answers, and the bar-open roster pass's memo of the kernel rosters that hold members and of the cohort receipts read (pineforge-engine#452), a function of the hashed rosters and receipts, emptied at every run begin; no value reaches a hash point. input_scheduler_ # Alias of the bound Pine host's scheduler, derived once at bind; reads the already-hashed input_is_observed_ticks_ provenance and owns no independent state. pine_view_host_ # The host the cached PineStrategyHost view was taken from (R5 lane V19-D, was a thread_local memo); a function of host_, reset at every bind and run begin. pine_view_ # The cached PineStrategyHost view of pine_view_host_ (dynamic_cast's answer); derived, never a decision input. @@ -82,3 +82,4 @@ symbol_calendar_opens_ # C-ABI transport of that calendar (syminfo metadata "sym waypoint_chain_bar_ # Transient within-bar value: the script bar whose path took a waypoint margin call (rest_waypoint_margin_call), set on that bar's path and cleared by the same bar's post-script close; compared only for equality with the current script bar, so it is dead at every bar boundary. script_average_held_ # Transient within-pass flag: hold_script_average_price sets it after a current-close exit flattened the book, and release_script_average_price clears it right after on_source_bar returns, before the pass's orders settle; it is false at every fold (the same class as security_history_publication_replay_). symbol_feeds_lent_ # Ownership flag: whether the installed symbol feeds' bars are held in symbol_feeds_ or lent to the kernel's run spec; the folds read the same keys, counts and digest either way and no decision reads it (AR-R4). +held_cohort_ids_ # Index of the ids whose cohort holds an opening or live units, in cohort_order_ order (pineforge-engine#452); a function of the hashed cohorts_by_id_ and cohort_order_, kept as they change and checked against them in every non-Release read; derived, never independent state. diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index af99883b2..c42db8590 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -1184,7 +1184,7 @@ void PineExecutionAdapter::initialize_l4c_policy(PlacementSnapshot& snapshot, std::string_view first_lot_id; std::uint64_t first_lot_incarnation = 0; - for (const auto& id : cohort_order_) { + for (const auto& id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(id); if (cohort == cohorts_by_id_.end()) continue; for (const auto& opening : cohort->second.opened) { @@ -2189,12 +2189,14 @@ void PineExecutionAdapter::purge_brackets_after_applied_reversal( (void)require_host().cancel(handle); retire(handle); } - for (auto& cohort : cohorts_by_id_) { - if (cohort_opened_on_side(cohort.second, prior_long)) { - cohort.second.opened.clear(); - cohort.second.live_units_by_origin.clear(); + for (const auto& id : held_cohort_ids()) { + auto& cohort = cohorts_by_id_.at(id); + if (cohort_opened_on_side(cohort, prior_long)) { + cohort.opened.clear(); + cohort.live_units_by_origin.clear(); } } + forget_emptied_cohorts(); } void PineExecutionAdapter::revive_brackets_after_margin( @@ -2398,11 +2400,14 @@ void PineExecutionAdapter::reset_for_run() { // Incarnations start over with the run. retired_row_scratch_.unsuperseded.clear(); retired_row_scratch_.roster_pass_due = true; + retired_row_scratch_.rostered.clear(); + retired_row_scratch_.cohort_receipts_read = 0; if (auto* index = adapter_lookup_index(bound_consumer(), this, run_counter_, false)) index->clear(); admission_journal.reset(); cohorts_by_id_.clear(); cohort_order_.clear(); + held_cohort_ids_.clear(); placement_.clear(); live_by_source_key_.clear(); bracket_families_.clear(); @@ -3553,8 +3558,10 @@ bool PineExecutionAdapter::lifecycle_readable(const PlacementSnapshot& row) cons if (origin == 0) return false; for (const auto& handle : live_handles_) if (handle.incarnation == origin) return true; - for (const auto& cohort : cohorts_by_id_) { - for (const auto& opening : cohort.second.opened) + for (const auto& id : held_cohort_ids()) { + const auto cohort = cohorts_by_id_.find(id); + if (cohort == cohorts_by_id_.end()) continue; + for (const auto& opening : cohort->second.opened) if (opening.incarnation == origin) return true; } return false; @@ -3659,8 +3666,10 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte // consumed-leg facts pruned below). auto& askable_origins = sweep.askable_origins; for (const auto& handle : live_handles_) askable_origins.push_back(handle.incarnation); - for (const auto& cohort : cohorts_by_id_) { - for (const auto& opening : cohort.second.opened) + for (const auto& id : held_cohort_ids()) { + const auto cohort = cohorts_by_id_.find(id); + if (cohort == cohorts_by_id_.end()) continue; + for (const auto& opening : cohort->second.opened) askable_origins.push_back(opening.incarnation); } std::sort(askable_origins.begin(), askable_origins.end()); @@ -3834,8 +3843,10 @@ void PineExecutionAdapter::erase_retired_rows(const NativeDecisionContext& conte }, command); return false; }); - for (const auto& cohort : cohorts_by_id_) { - for (const auto& opening : cohort.second.opened) root(opening.incarnation); + for (const auto& id : held_cohort_ids()) { + const auto cohort = cohorts_by_id_.find(id); + if (cohort == cohorts_by_id_.end()) continue; + for (const auto& opening : cohort->second.opened) root(opening.incarnation); } if (auto* pine = pine_view_of(host_)) { for (const auto& lot : pine->pyramid_entries_) root(lot.entry_incarnation); @@ -4091,8 +4102,47 @@ void PineExecutionAdapter::release_closed_cohort_origins() { auto* pine = pine_view_of(host_); if (!pine) return; const auto& core = as_native_consumer(*consumer).request_core(); + // The rosters that hold members, in handle order (core.cohorts()'s): + // every loop below reads a roster only through its members, so these + // alone answer what walking every roster the run opened would. + auto& rostered = scratch.rostered; + const auto& receipts = core.cohort_receipts(); + if (receipts.size() < scratch.cohort_receipts_read) { + rostered.clear(); + scratch.cohort_receipts_read = 0; + } + for (; scratch.cohort_receipts_read < receipts.size(); ++scratch.cohort_receipts_read) { + const auto& receipt = receipts[scratch.cohort_receipts_read]; + if (receipt.operation != native_order::CohortReceiptOperation::Add + || receipt.status != native_order::CohortReceiptStatus::Applied) { + continue; + } + const auto at = std::lower_bound(rostered.begin(), rostered.end(), receipt.cohort); + if (at == rostered.end() || *at != receipt.cohort) rostered.insert(at, receipt.cohort); + } + const auto roster_of = [&](native_order::CohortHandle handle) { + const auto& all = core.cohorts(); + const auto at = std::lower_bound(all.begin(), all.end(), handle, + [](const native_order::CohortRoster& roster, native_order::CohortHandle value) { + return roster.handle < value; + }); + return at != all.end() && at->handle == handle ? &*at : nullptr; + }; + auto& rosters = scratch.held_rosters; + rosters.clear(); + rostered.erase(std::remove_if(rostered.begin(), rostered.end(), + [&](native_order::CohortHandle handle) { + const auto* roster = roster_of(handle); + if (!roster || roster->origins.empty()) return true; + rosters.push_back(roster); + return false; + }), rostered.end()); + assert(std::count_if(core.cohorts().begin(), core.cohorts().end(), + [](const native_order::CohortRoster& roster) { + return !roster.origins.empty(); + }) == static_cast(rosters.size())); std::size_t members = 0; - for (const auto& roster : core.cohorts()) members += roster.origins.size(); + for (const auto* roster : rosters) members += roster->origins.size(); if (members == 0) return; // The common case, in a direct scan with nothing allocated or sorted: // every member still opened a lot that is open, or roots a working @@ -4104,8 +4154,8 @@ void PineExecutionAdapter::release_closed_cohort_origins() { constexpr std::size_t kDirectScan = 64; if (members * (pine->pyramid_entries_.size() + core.live().size()) <= kDirectScan) { bool every_member_bound = true; - for (const auto& roster : core.cohorts()) { - for (const auto& origin : roster.origins) { + for (const auto* roster : rosters) { + for (const auto& origin : roster->origins) { const std::uint64_t incarnation = origin.incarnation; const bool bound = std::any_of(pine->pyramid_entries_.begin(), pine->pyramid_entries_.end(), [&](const PyramidEntry& lot) { @@ -4150,8 +4200,8 @@ void PineExecutionAdapter::release_closed_cohort_origins() { for (const auto opening : lots) { if (opening == 0) continue; bool root = false; - for (const auto& roster : core.cohorts()) { - if (member(roster.origins, opening)) { + for (const auto* roster : rosters) { + if (member(roster->origins, opening)) { root = true; break; } @@ -4159,7 +4209,8 @@ void PineExecutionAdapter::release_closed_cohort_origins() { if (!root) successor_lots.push_back(opening); } auto& probe = scratch.lot_probe; - for (const auto& roster : core.cohorts()) { + for (const auto* roster_row : rosters) { + const auto& roster = *roster_row; const auto& origins = roster.origins; if (origins.empty()) continue; if (probe.run != origins.front().run) probe.run = origins.front().run; @@ -4187,6 +4238,11 @@ void PineExecutionAdapter::release_closed_cohort_origins() { origin.incarnation = incarnation; host.cohort_remove(cohort, std::move(origin)); } + rostered.erase(std::remove_if(rostered.begin(), rostered.end(), + [&](native_order::CohortHandle handle) { + const auto* roster = roster_of(handle); + return !roster || roster->origins.empty(); + }), rostered.end()); } void PineExecutionAdapter::maybe_activate_short_seed_plan() { @@ -4200,7 +4256,7 @@ bool PineExecutionAdapter::short_seed_context_is_live() const noexcept { const auto physical = detail::run_position(*host_); if (!(physical.signed_units < 0.0) || physical.lot_count != 1U) return false; const double held = std::abs(physical.signed_units); - for (const auto& id : cohort_order_) { + for (const auto& id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(id); if (cohort == cohorts_by_id_.end()) continue; if (cohort_exposure_for(id) == held) return true; @@ -5074,15 +5130,84 @@ std::optional PineExecutionAdapter::from_entry_units( } int PineExecutionAdapter::source_entry_slot_count() const noexcept { + const auto walk = [&] { + std::size_t walked = 0; + for (const auto& id : cohort_order_) { + const auto found = cohorts_by_id_.find(id); + if (found != cohorts_by_id_.end()) walked += found->second.opened.size(); + } + return walked; + }; + (void)walk; std::size_t total = 0; - for (const auto& id : cohort_order_) { + for (const auto& id : held_cohort_ids()) { const auto found = cohorts_by_id_.find(id); if (found != cohorts_by_id_.end()) total += found->second.opened.size(); } + assert(total == walk()); return total > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : static_cast(total); } +void PineExecutionAdapter::hold_cohort(const SourceId& id) { + if (std::find(held_cohort_ids_.begin(), held_cohort_ids_.end(), id) != held_cohort_ids_.end()) + return; + const auto handle_of = [&](const SourceId& held) { + const auto found = cohorts_by_id_.find(held); + return found == cohorts_by_id_.end() ? 0 : found->second.handle.value; + }; + const std::uint64_t handle = handle_of(id); + auto at = held_cohort_ids_.begin(); + while (at != held_cohort_ids_.end() && handle_of(*at) <= handle) ++at; + held_cohort_ids_.insert(at, id); +} + +void PineExecutionAdapter::forget_emptied_cohorts() { + held_cohort_ids_.erase( + std::remove_if(held_cohort_ids_.begin(), held_cohort_ids_.end(), + [&](const SourceId& id) { + const auto found = cohorts_by_id_.find(id); + return found == cohorts_by_id_.end() + || (found->second.opened.empty() + && found->second.live_units_by_origin.empty()); + }), + held_cohort_ids_.end()); +} + +const std::vector& PineExecutionAdapter::held_cohort_ids() const noexcept { + assert(held_cohort_ids_exact()); + return held_cohort_ids_; +} + +// The index names each cohort that holds an opening or live units once, in +// cohort_order_ order, and nothing else; so a walk over it visits, in the +// same order, every cohort a walk over cohort_order_ or cohorts_by_id_ finds +// anything in. +bool PineExecutionAdapter::held_cohort_ids_exact() const noexcept { + std::size_t holding = 0; + for (const auto& cohort : cohorts_by_id_) + if (!cohort.second.opened.empty() || !cohort.second.live_units_by_origin.empty()) + ++holding; + if (holding != held_cohort_ids_.size()) return false; + auto position = cohort_order_.begin(); + for (const auto& id : held_cohort_ids_) { + const auto found = cohorts_by_id_.find(id); + if (found == cohorts_by_id_.end()) return false; + if (found->second.opened.empty() && found->second.live_units_by_origin.empty()) + return false; + position = std::find(position, cohort_order_.end(), id); + if (position == cohort_order_.end()) return false; + ++position; + } + std::uint64_t previous = 0; + for (const auto& id : cohort_order_) { + const auto handle = cohorts_by_id_.at(id).handle.value; + if (!(handle > previous)) return false; + previous = handle; + } + return true; +} + double PineExecutionAdapter::percent_commission_live_equity(double mark) const noexcept { return percent_commission_live_equity(mark, std::nullopt); } @@ -5895,6 +6020,7 @@ void PineExecutionAdapter::consume_cohort_units( if (unit->second == 0.0) facts.live_units_by_origin.erase(unit); if (!(remaining > 0.0)) break; } + forget_emptied_cohorts(); } void PineExecutionAdapter::consume_closed_trade_rows( @@ -5936,22 +6062,34 @@ void PineExecutionAdapter::consume_closed_trade_rows( if (index >= static_cast(host.trade_count())) continue; const Trade& trade = host.get_trade(static_cast(index)); double remaining = trade.qty; - bool matched = false; - for (auto& cohort : cohorts_by_id_) { - auto units = cohort.second.live_units_by_origin.find( - trade.entry_incarnation); - if (units == cohort.second.live_units_by_origin.end()) continue; + // An opening's units live in its own id's cohort alone, so the first + // holder in cohort order is the one any walk finds. + const auto holds_entry = [&](const CohortFacts& cohort) { + return cohort.live_units_by_origin.count(trade.entry_incarnation) != 0; + }; + assert(std::count_if(cohorts_by_id_.begin(), cohorts_by_id_.end(), + [&](const auto& cohort) { return holds_entry(cohort.second); }) <= 1); + auto holder = cohorts_by_id_.end(); + for (const auto& id : held_cohort_ids()) { + const auto cohort = cohorts_by_id_.find(id); + if (cohort != cohorts_by_id_.end() && holds_entry(cohort->second)) { + holder = cohort; + break; + } + } + if (holder != cohorts_by_id_.end()) { + auto units = holder->second.live_units_by_origin.find(trade.entry_incarnation); const double consumed = std::min(units->second, remaining); units->second -= consumed; remaining -= consumed; const bool drained = !(units->second > 1e-10); if (drained) - cohort.second.live_units_by_origin.erase(units); - settle_slot(cohort.second, trade, drained); - matched = true; - break; + holder->second.live_units_by_origin.erase(units); + settle_slot(holder->second, trade, drained); + forget_emptied_cohorts(); + continue; } - if (matched || !(remaining > 0.0)) continue; + if (!(remaining > 0.0)) continue; const auto cohort = cohorts_by_id_.find(trade.entry_id); if (cohort == cohorts_by_id_.end()) continue; // False once nothing remains to consume. @@ -5965,6 +6103,7 @@ void PineExecutionAdapter::consume_closed_trade_rows( if (drained) cohort->second.live_units_by_origin.erase(units); settle_slot(cohort->second, trade, drained); + forget_emptied_cohorts(); return remaining > 0.0; }; auto* lookup = cohort->second.origins.size() <= kIndexedFrom ? nullptr @@ -6920,7 +7059,7 @@ bool PineExecutionAdapter::coof_current_fill_was_forced_waypoint() const noexcep if (!coof_recalc_active_) return false; const auto point = detail::callback_point(require_host()); if (!point) return false; - for (const auto& id : cohort_order_) { + for (const auto& id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(id); if (cohort == cohorts_by_id_.end()) continue; for (const auto& opening : cohort->second.opened) { @@ -7117,9 +7256,7 @@ void PineExecutionAdapter::flush_coof_tail( std::size_t PineExecutionAdapter::accepted_entries_in_cycle( bool is_long, const SourceId& id) const { std::size_t accepted_in_cycle = 0; - std::vector cohort_ids; - cohort_ids.reserve(cohorts_by_id_.size()); - for (const auto& row : cohorts_by_id_) cohort_ids.push_back(row.first); + std::vector cohort_ids = held_cohort_ids(); std::sort(cohort_ids.begin(), cohort_ids.end()); for (const auto& cohort_id : cohort_ids) { const auto cohort = cohorts_by_id_.find(cohort_id); @@ -8324,9 +8461,7 @@ void PineExecutionAdapter::place_entry(const SourceId& id, bool is_long, double } std::vector carried_brackets; std::vector carried_ids; - std::vector cohort_ids; - cohort_ids.reserve(cohorts_by_id_.size()); - for (const auto& row : cohorts_by_id_) cohort_ids.push_back(row.first); + std::vector cohort_ids = held_cohort_ids(); std::sort(cohort_ids.begin(), cohort_ids.end()); for (const auto& cohort_id : cohort_ids) { const auto cohort = cohorts_by_id_.find(cohort_id); @@ -9293,8 +9428,8 @@ void PineExecutionAdapter::flush_pending_closes() { if (incarnation == 0) return std::nullopt; if (!included.insert(incarnation).second) continue; const native_order::RequestHandle* receipt = nullptr; - for (const auto& cohort : cohorts_by_id_) { - for (const auto& opened : cohort.second.opened) + for (const auto& id : held_cohort_ids()) { + for (const auto& opened : cohorts_by_id_.at(id).opened) if (opened.incarnation == incarnation) receipt = &opened; } if (!receipt) return std::nullopt; @@ -12812,8 +12947,8 @@ void PineExecutionAdapter::flush_pending_bracket_legs( // (src/source/pine_strategy_commands.cpp:481-482, // src/source/pine_strategy_commands.cpp:2627-2648). This route carries the // same fact on the snapshot's source sequence, which a re-issued bracket - // inherits (src/source/pine_adapter.cpp:6601) and the book walk already - // ranks on (src/source/pine_adapter.cpp:11288). + // inherits (src/source/pine_adapter.cpp:6740) and the book walk already + // ranks on (src/source/pine_adapter.cpp:11423). const auto parent_precedes_leg = [](const PlacementSnapshot& leg, const PlacementSnapshot& parent) { return parent.source_sequence < leg.source_sequence; @@ -12992,7 +13127,7 @@ void PineExecutionAdapter::flush_pending_bracket_legs( // the leg, so the leg settles with the position open ahead of the body. A // leg whose parent entry was the fill that opened the position has already // had that parent's calculation, so the walk also satisfies the deferred - // post-parent-calculation parking flag (pine_adapter.cpp:7393). + // post-parent-calculation parking flag (pine_adapter.cpp:7530). const auto parent_walks_first = [&](const PlacementSnapshot& leg) { if (!pre_script_drain) return false; @@ -15931,7 +16066,7 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_source_terms( const bool keep_mc_close_surplus = [&]() { if (!source.affordability_keep_mc_close_surplus) return false; bool receipt_origin_live = false; - for (const auto& cohort_id : cohort_order_) { + for (const auto& cohort_id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(cohort_id); if (cohort == cohorts_by_id_.end()) continue; const auto units = cohort->second.live_units_by_origin.find( @@ -17789,7 +17924,7 @@ bool PineExecutionAdapter::source_margin_rounded_tie_veto() const { return false; } bool rounded_tie_opening = false; - for (const auto& cohort_id : cohort_order_) { + for (const auto& cohort_id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(cohort_id); if (cohort == cohorts_by_id_.end()) continue; for (const auto& origin : cohort->second.opened) { @@ -18416,10 +18551,11 @@ bool PineExecutionAdapter::slipped_long_book_scope() const { } } const CohortFacts* sole_cohort = nullptr; - for (const auto& row : cohorts_by_id_) { - if (row.second.opened.empty()) continue; + for (const auto& id : held_cohort_ids()) { + const auto& row = cohorts_by_id_.at(id); + if (row.opened.empty()) continue; if (sole_cohort != nullptr) return false; - sole_cohort = &row.second; + sole_cohort = &row; } if (sole_cohort == nullptr || sole_cohort->opened.size() != 1) return false; const auto placement = placement_.find(sole_cohort->opened.front().incarnation); @@ -18748,10 +18884,11 @@ bool PineExecutionAdapter::slipped_pooc_opening_money_scope( } const CohortFacts* sole_cohort = nullptr; - for (const auto& row : cohorts_by_id_) { - if (row.second.opened.empty()) continue; + for (const auto& id : held_cohort_ids()) { + const auto& row = cohorts_by_id_.at(id); + if (row.opened.empty()) continue; if (sole_cohort != nullptr) return false; - sole_cohort = &row.second; + sole_cohort = &row; } if (sole_cohort == nullptr || sole_cohort->opened.size() != 1 || sole_cohort->live_units_by_origin.size() != 1) { @@ -19004,8 +19141,8 @@ std::optional PineExecutionAdapter::flat_sibling_placement( const auto position = detail::run_position(require_host()); if (position.signed_units == 0.0 || position.lot_count < min_lots) return std::nullopt; std::optional placed; - for (const auto& cohort : cohorts_by_id_) { - for (const auto& [incarnation, units] : cohort.second.live_units_by_origin) { + for (const auto& id : held_cohort_ids()) { + for (const auto& [incarnation, units] : cohorts_by_id_.at(id).live_units_by_origin) { if (!(units > 0.0)) continue; const auto found = placement_.find(incarnation); if (found == placement_.end()) return std::nullopt; @@ -19098,8 +19235,8 @@ std::size_t PineExecutionAdapter::add_sibling_lots(std::int64_t placed) const { const auto position = detail::run_position(require_host()); if (position.signed_units == 0.0) return 0; std::size_t count = 0; - for (const auto& cohort : cohorts_by_id_) { - for (const auto& [incarnation, units] : cohort.second.live_units_by_origin) { + for (const auto& id : held_cohort_ids()) { + for (const auto& [incarnation, units] : cohorts_by_id_.at(id).live_units_by_origin) { if (!(units > 0.0)) continue; const auto found = placement_.find(incarnation); if (found == placement_.end()) continue; @@ -24715,10 +24852,12 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& std::sort(closed_cohorts.begin(), closed_cohorts.end()); for (const auto& id : closed_cohorts) cancel_exit_orders_for_full_close(id); - for (auto& cohort : cohorts_by_id_) { - cohort.second.opened.clear(); - cohort.second.live_units_by_origin.clear(); + for (const auto& id : held_cohort_ids()) { + auto& cohort = cohorts_by_id_.at(id); + cohort.opened.clear(); + cohort.live_units_by_origin.clear(); } + held_cohort_ids_.clear(); // ab9714be pine_strategy_host.cpp:244 / :261 // reset_source_open_position_ledgers_before_book clears the close // ledgers when a position is opened or reversed, so unclosed units of @@ -24882,6 +25021,7 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& facts.opened.push_back(event.handle()); facts.live_units_by_origin[event.handle().incarnation] += std::abs(event.opened_units); + hold_cohort(placement_snapshot->source_id); } credit_close_ledger(placement_snapshot->source_id, std::abs(event.opened_units)); credit_pyramiding_record(placement_snapshot->source_id, event.handle().incarnation, @@ -25282,7 +25422,7 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& const auto physical = detail::run_position(require_host()); std::uint64_t sole_live_entry_incarnation = 0; std::size_t live_entry_count = 0; - for (const auto& id : cohort_order_) { + for (const auto& id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(id); if (cohort == cohorts_by_id_.end()) continue; for (const auto& opening : cohort->second.opened) { @@ -25318,7 +25458,7 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& std::uint64_t sole_entry_incarnation = 0; int live_entry_origins = 0; if (physical.signed_units != 0.0 && physical.lot_count == 1U) { - for (const auto& cohort_id : cohort_order_) { + for (const auto& cohort_id : held_cohort_ids()) { const auto cohort = cohorts_by_id_.find(cohort_id); if (cohort == cohorts_by_id_.end()) continue; for (const auto& origin : cohort->second.opened) { @@ -25428,7 +25568,9 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& if (placement_snapshot && placement_snapshot->opening) { const double physical_exposure = std::abs(detail::run_position(require_host()).signed_units); - for (const auto& id : cohort_order_) { + // A copy: the reconciliation may cancel and re-place exit orders. + const auto held = held_cohort_ids(); + for (const auto& id : held) { const double exposure = cohort_exposure_for(id); const bool paired_source_transaction = placement_snapshot->paired_flat_market_candidate @@ -25535,7 +25677,9 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& } if (placement_snapshot && placement_snapshot->family == PineOrderFamily::CloseAll && event.closed_units > 0.0) { - for (auto& cohort : cohorts_by_id_) cohort.second.live_units_by_origin.clear(); + for (const auto& id : held_cohort_ids()) + cohorts_by_id_.at(id).live_units_by_origin.clear(); + forget_emptied_cohorts(); } if (placement_snapshot && placement_snapshot->family == PineOrderFamily::Margin && event.closed_units > 0.0 @@ -25657,11 +25801,13 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& exit_tombstones_.clear(); std::vector ended_sources; pending_margin_revivals_.clear(); - for (auto& cohort : cohorts_by_id_) { - if (!cohort.second.opened.empty()) ended_sources.push_back(cohort.first); - cohort.second.opened.clear(); - cohort.second.live_units_by_origin.clear(); + for (const auto& id : held_cohort_ids()) { + auto& cohort = cohorts_by_id_.at(id); + if (!cohort.opened.empty()) ended_sources.push_back(id); + cohort.opened.clear(); + cohort.live_units_by_origin.clear(); } + held_cohort_ids_.clear(); const auto handles = live_handles_; for (const auto& handle : handles) { const auto found = placement_.find(handle.incarnation); diff --git a/tests/ownership/probes/engine/margin_maintenance_overflow/NOTES.md b/tests/ownership/probes/engine/margin_maintenance_overflow/NOTES.md index 76aabed1a..acad7ff08 100644 --- a/tests/ownership/probes/engine/margin_maintenance_overflow/NOTES.md +++ b/tests/ownership/probes/engine/margin_maintenance_overflow/NOTES.md @@ -10,4 +10,4 @@ The expectation is the contract row (revision 3: a value derived by the engine f The native boundary control names the requested source-free test TU. Its relevant native producer/control is an arithmetic counterpart; source-specific cash/equity/placement checks are in the proposed source companion. No source probe claims coverage of the internal pending-deduction corruption used for D6 row 6. -Not covered here, and an open question for the contract owner: a 1x long (`margin_long=100`, the default) takes no kernel maintenance check at bar open (`src/source/pine_adapter.cpp:22524-22525`, `:22632`). The only replacement call (`:18548-18559`) returns early when ANY of these holds: no lot grid, point value other than 1, commission other than 0, slippage other than 0, an FX schedule, or risk rules; and a nonfinite value there becomes "no call" with no typed fault (`:17594`, `:17602-17607`). So this row is witnessed only on the checked route (margin other than 100, or a short). Measured on a box with scratch probes (not part of this tree): a 1x long with point value 1e300 and a finite total (0.5 units, price 2.5e8) completes normally; a 1x long whose total overflows (2 units, price 2.5e8, capital 1e301) fails at the end of the run with `engine_invariant` ("non-finite open-position report row"), not with a typed `margin.maintenance` fault. +Not covered here, and an open question for the contract owner: a 1x long (`margin_long=100`, the default) takes no kernel maintenance check at bar open (`src/source/pine_adapter.cpp:22661-22662`, `:22632`). The only replacement call (`:18548-18559`) returns early when ANY of these holds: no lot grid, point value other than 1, commission other than 0, slippage other than 0, an FX schedule, or risk rules; and a nonfinite value there becomes "no call" with no typed fault (`:17594`, `:17602-17607`). So this row is witnessed only on the checked route (margin other than 100, or a short). Measured on a box with scratch probes (not part of this tree): a 1x long with point value 1e300 and a finite total (0.5 units, price 2.5e8) completes normally; a 1x long whose total overflows (2 units, price 2.5e8, capital 1e301) fails at the end of the run with `engine_invariant` ("non-finite open-position report row"), not with a typed `margin.maintenance` fault. diff --git a/tests/ownership/probes/engine/margin_required_explicit_overflow/NOTES.md b/tests/ownership/probes/engine/margin_required_explicit_overflow/NOTES.md index d78f12d0c..83cffcda4 100644 --- a/tests/ownership/probes/engine/margin_required_explicit_overflow/NOTES.md +++ b/tests/ownership/probes/engine/margin_required_explicit_overflow/NOTES.md @@ -8,4 +8,4 @@ Feed: 40 synthetic one-minute bars, starting at 1736121600000 ms, volume 1. Each The box run b4/b5 observation (completed, 0 trades, equity constant) is consistent with the ruling. The L6 10-14 ruling (`f2a455b14`) determines this expectation; the observation only corroborates it. -The native boundary control is the existing `tests/test_value_invalid_sizing_margin_source.cpp`: the L6 split is pinned at lines 196-197, and `keep_finite_explicit_placement_overflow()` at lines 232-262 checks completion and callback continuation after refusing the overflowing entry. The placement boundary is `src/source/pine_adapter.cpp:7365-7378`; with margin 100, an explicit fixed market entry from flat, point value 1 and fx 1, the order is in the slipped-signal scope and is refused at lines 7391-7421 (the checks at lines 7430-7445 cover the other scopes). No source probe claims coverage of the internal pending-deduction corruption used for D6 row 6. +The native boundary control is the existing `tests/test_value_invalid_sizing_margin_source.cpp`: the L6 split is pinned at lines 196-197, and `keep_finite_explicit_placement_overflow()` at lines 232-262 checks completion and callback continuation after refusing the overflowing entry. The placement boundary is `src/source/pine_adapter.cpp:7502-7515`; with margin 100, an explicit fixed market entry from flat, point value 1 and fx 1, the order is in the slipped-signal scope and is refused at lines 7391-7421 (the checks at lines 7430-7445 cover the other scopes). No source probe claims coverage of the internal pending-deduction corruption used for D6 row 6. diff --git a/tests/test_adapter_grid_relower.cpp b/tests/test_adapter_grid_relower.cpp index 96d94e960..d4701deda 100644 --- a/tests/test_adapter_grid_relower.cpp +++ b/tests/test_adapter_grid_relower.cpp @@ -436,7 +436,7 @@ std::vector feed(Shape shape) { push(14.01, 14.02, 14.00, 14.01); // 3 break; case Shape::SubTickSellLimit: - // pine_adapter.cpp:7830-7834: NYSE:F high 10.175 -> 10.18 fills a + // pine_adapter.cpp:7967-7971: NYSE:F high 10.175 -> 10.18 fills a // 10.18 sell limit. push(10.10, 10.10, 10.10, 10.10); // 0: entry placed push(10.10, 10.12, 10.08, 10.10); // 1: L fills @10.10, limit 10.18 placed diff --git a/tests/test_adapter_live_state_scaling.cpp b/tests/test_adapter_live_state_scaling.cpp index 31ef09056..3354581ad 100644 --- a/tests/test_adapter_live_state_scaling.cpp +++ b/tests/test_adapter_live_state_scaling.cpp @@ -132,8 +132,24 @@ // of CI's size (the large leg 27.5 % dearer a bar) the blocks' window is four // small runs longer than the rounds': on that runner about 0.8 s against 0.1. // +// Spaced unique ids (pineforge-engine#452), without recording: an opening +// every 20 bars, each under an id of its own the way scripts that tag every +// trade with its bar or time spell `strategy.entry`, closed 10 bars later. +// The adapter keeps a cohort for every id the run has used, and the kernel a +// roster for each. Walks over all of them ran on every bar (the open-entry +// count the pyramiding gate reads, the openings erase_retired_rows() +// collects, the bar-open roster pass) and at every fill and placement, so a +// run cost the square of its trades; they now read only the cohorts that +// hold an opening or live units and the rosters that hold members. The row +// closes each opening before the next, so no fill reverses the position: a +// reversal still cancels the exits of every id the run has used +// (on_applied). Fail-before, this TU against the base 0212cabe with one +// attempt (macOS arm64, Apple clang 17, Release): 32,000 bars 0.3703 s, +// 128,000 bars 8.4500 s, ratio 22.82 (bound 5.0); after, 128,000 bars +// 0.1115 s, 512,000 bars 0.4546 s, ratio 4.08. +// // Single-leg mode for peak-RSS rows: `test_adapter_live_state_scaling --leg -// replay|churn|straddle|recancel|constant|cancelall|flip +// replay|churn|straddle|recancel|constant|cancelall|flip|uniqueids // [--no-recording]` runs one leg and prints its CPU time, retained rows and // ru_maxrss. #include @@ -178,7 +194,7 @@ int failures = 0; constexpr double kNa = std::numeric_limits::quiet_NaN(); constexpr std::int64_t T = 1736121600000LL; -enum class Workload { Replay, Churn, Straddle, Recancel, Constant, CancelAll, Flip }; +enum class Workload { Replay, Churn, Straddle, Recancel, Constant, CancelAll, Flip, UniqueIds }; long triangle(int index, int period) { const int phase = index % (2 * period); @@ -266,6 +282,13 @@ class ScalingHost final : public source::PineStrategyHost { else strategy_close("L"); return; } + if (workload_ == Workload::UniqueIds) { + // An opening every 20 bars under an id of its own, closed 10 + // bars later. + if (i % 20 == 0) strategy_entry("L" + std::to_string(i), true, kNa, kNa, 1.0); + else if (i % 20 == 10) strategy_close("L" + std::to_string(i - 10)); + return; + } if (workload_ == Workload::Straddle) { // One trade, then one flat position cycle for the rest of the run: // the entry opens at bar 8 and its take-profit, a quarter above the @@ -391,7 +414,7 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording // The straddle and the re-cancels run four times longer: their bars cost // microseconds. const bool lane_rows = workload == Workload::Constant || workload == Workload::CancelAll - || workload == Workload::Flip; + || workload == Workload::Flip || workload == Workload::UniqueIds; const bool short_bars = workload == Workload::Straddle || workload == Workload::Recancel || lane_rows; const int scale = short_bars ? 4 : 1; @@ -431,6 +454,11 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording CHECK(small.trades == 0); CHECK(large.trades == 0); } + if (workload == Workload::UniqueIds) { + // One round trip per 20 bars; the last opening may still be open. + CHECK(small.trades >= bars / 20 - 1); + CHECK(large.trades >= bars / 5 - 1); + } if (workload == Workload::Flip) { // One round trip per two bars; the last opening may still be open. CHECK(small.trades >= bars / 2 - 1); @@ -458,6 +486,7 @@ int single_leg(const char* workload_name, const char* bars_text, bool recording) : std::strcmp(workload_name, "constant") == 0 ? Workload::Constant : std::strcmp(workload_name, "cancelall") == 0 ? Workload::CancelAll : std::strcmp(workload_name, "flip") == 0 ? Workload::Flip + : std::strcmp(workload_name, "uniqueids") == 0 ? Workload::UniqueIds : Workload::Replay; const int bars = std::atoi(bars_text); if (bars <= 0) return 2; @@ -475,7 +504,8 @@ int single_leg(const char* workload_name, const char* bars_text, bool recording) } // namespace int main(int argc, char** argv) { - // --leg [--no-recording] + // --leg + // [--no-recording] if ((argc == 4 || argc == 5) && std::strcmp(argv[1], "--leg") == 0) return single_leg(argv[2], argv[3], !(argc == 5 && std::strcmp(argv[4], "--no-recording") == 0)); @@ -499,6 +529,8 @@ int main(int argc, char** argv) { row("cancelall", Workload::CancelAll, "cancel_all and an exit at the entry price x 0.95 every bar", false); row("flip", Workload::Flip, "an opening every second bar, closed on the next"); + row("uniqueids", Workload::UniqueIds, + "an opening every 20 bars under an id of its own, closed 10 bars later", false); if (failures == 0) std::printf("test_adapter_live_state_scaling: ok\n"); return failures == 0 ? 0 : 1; } diff --git a/tests/test_adapter_sizing_relower.cpp b/tests/test_adapter_sizing_relower.cpp index fb40b7be6..536f5c3be 100644 --- a/tests/test_adapter_sizing_relower.cpp +++ b/tests/test_adapter_sizing_relower.cpp @@ -604,7 +604,7 @@ void a_default_sized_reversal_keeps_both_rows() { // // The core resolves a ScopeFraction as units = scope * fraction, one binary64 // multiplication. TradingView resolves basis * percent / 100.0 -// (src/source/pine_adapter.cpp:2545) and then applies its own percentage-exit +// (src/source/pine_adapter.cpp:2550) and then applies its own percentage-exit // quantizer (:2550-2565, with a +1e-6 floor and a keep-one-lot rule). The two // agree on the integer lot below and disagree on a fractional basis, so the // percentage exits are NOT re-lowered. diff --git a/tests/test_native_anchored_legs.cpp b/tests/test_native_anchored_legs.cpp index 1ae7ec8f3..475c2d6a5 100644 --- a/tests/test_native_anchored_legs.cpp +++ b/tests/test_native_anchored_legs.cpp @@ -1066,7 +1066,7 @@ void facts_resolve_at_the_arm() { // directional_tick src/source/pine_adapter.cpp:297-301 // source_bar_fill_tick / nearest_tick are not needed on these on-grid levels // source_trigger_threshold src/source/pine_adapter.cpp:328-375 -// the fill booking src/source/pine_adapter.cpp:9907-9926 (an +// the fill booking src/source/pine_adapter.cpp:10042-10061 (an // ExitLimit books directional_tick(resolved, // !is_buy) clamped to its level; an ExitStop / // ExitTrail books directional_tick(resolved, @@ -1259,9 +1259,9 @@ class TwinHost final : public NativeStrategyHost { } // The adapter's fill booking: an exit crossed intrabar at a half-tick - // threshold books the grid level (pine_adapter.cpp:9907-9926); the + // threshold books the grid level (pine_adapter.cpp:10042-10061); the // explicit-zero trail books the next open (resolve_terms - // explicit_zero_trail, pine_adapter.cpp:9466-9476 and its + // explicit_zero_trail, pine_adapter.cpp:9601-9611 and its // zero_trail_policy_price). no::ExecutionTerms resolve_execution_terms(const NativeExecutionTermsFacts& facts) const override { no::ExecutionTerms terms{facts.default_resolved_price, std::nullopt, diff --git a/tests/test_native_margin_hooks_twin.cpp b/tests/test_native_margin_hooks_twin.cpp index 6cf12e7f1..9fade4599 100644 --- a/tests/test_native_margin_hooks_twin.cpp +++ b/tests/test_native_margin_hooks_twin.cpp @@ -172,7 +172,7 @@ struct TvNativeHost final : Host { void on_native_margin_call(const no::MarginCallEvent& event) override { margin_calls.push_back(event); } - // MG-B: pine_adapter.cpp:11491-11499. The requirement is money, and this + // MG-B: pine_adapter.cpp:11626-11634. The requirement is money, and this // broker keeps money to ten significant digits whenever a lot is worth // less than one unit of account. std::optional resolve_margin_requirement( @@ -185,7 +185,7 @@ struct TvNativeHost final : Host { decision.equity = view.equity; return decision; } - // MG-F / MG-G: pine_adapter.cpp:11505-11533, on the kernel's own facts. + // MG-F / MG-G: pine_adapter.cpp:11640-11668, on the kernel's own facts. std::optional resolve_margin_call_units( const NativeMarginCallView& view) const override { const double unit_margin = view.mark * fraction(); @@ -213,7 +213,7 @@ struct TvNativeHost final : Host { units = std::min(book, units); return units > 1e-10 && std::isfinite(units) ? units : 0.0; } - // MG-H: pine_adapter.cpp:11588-11597. The slice fires at the waypoint the + // MG-H: pine_adapter.cpp:11723-11732. The slice fires at the waypoint the // kernel rested it on; the booked price is that fire price on the tick // ladder plus the EXIT side's own market slippage. no::ExecutionTerms resolve_execution_terms( diff --git a/tests/test_native_margin_model.cpp b/tests/test_native_margin_model.cpp index 77c522dec..ff0c6677b 100644 --- a/tests/test_native_margin_model.cpp +++ b/tests/test_native_margin_model.cpp @@ -718,7 +718,7 @@ void maintenance_only_model() { // The native model with initial = maintenance = 0.5 and ShortfallMultiple 4.0 // liquidates on the SAME bar, on the same side, for the SAME units. The only // difference is the booked price, and it is a named TV quirk (MG15, fill -// pricing pine_adapter.cpp:11588-11597): the adapter pins its slice to the +// pricing pine_adapter.cpp:11723-11732): the adapter pins its slice to the // bar's adverse extreme it sized against, while the generic kernel rests the // reduction at the liquidation level and books it where the account actually // runs out of margin. diff --git a/tests/test_native_risk_limits.cpp b/tests/test_native_risk_limits.cpp index 63aaf85f1..6ad939575 100644 --- a/tests/test_native_risk_limits.cpp +++ b/tests/test_native_risk_limits.cpp @@ -710,19 +710,19 @@ void configuration_is_validated() { // // Itemized differences, each a named TradingView quirk: // * MG14 (day key): TV keys the day on chart_day_key, the composite -// mday*100+month of the chart timezone (pine_adapter.cpp:11281-11305). +// mday*100+month of the chart timezone (pine_adapter.cpp:11416-11440). // The kernel keys on the calendar day ordinal of the spec timezone. // Same boundary here, different key. // * MG12 (cancel-pending): TV's forced close also latches // intraday_cancel_pending and withdraws the working orders -// (pine_adapter.cpp:12400-12409). The kernel blocks openings and +// (pine_adapter.cpp:12535-12544). The kernel blocks openings and // leaves every working order where it is. // * MG12 (fill pricing and comment): TV submits its Flatten with // NearestTick pricing and the legacy comment "Close Position (Max // intraday Loss)"; the kernel books AsPresented at the evaluation // point under its own engine-owned label. // * MG12 (threshold epsilon): TV compares loss + 1e-9*max(1,|limit|) -// >= limit (pine_adapter.cpp:11427-11429); the kernel compares +// >= limit (pine_adapter.cpp:11562-11564); the kernel compares // exactly. Both fire at -1200 against 1000. // * MG12 (unbooked closing fill): TV excludes the closing fill's own // realized P&L at its own tick; the kernel marks equity as it stands.